tor-browser

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

test_GuardianClient.js (13470B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { HttpServer, HTTP_404 } = ChromeUtils.importESModule(
      7  "resource://testing-common/httpd.sys.mjs"
      8 );
      9 const { GuardianClient } = ChromeUtils.importESModule(
     10  "moz-src:///browser/components/ipprotection/GuardianClient.sys.mjs"
     11 );
     12 
     13 function makeGuardianServer(
     14  arg = {
     15    enroll: (_request, _response) => {},
     16    token: (_request, _response) => {},
     17    status: (_request, _response) => {},
     18  }
     19 ) {
     20  const callbacks = {
     21    enroll: (_request, _response) => {},
     22    token: (_request, _response) => {},
     23    status: (_request, _response) => {},
     24    ...arg,
     25  };
     26  const server = new HttpServer();
     27 
     28  server.registerPathHandler("/api/v1/fpn/token", callbacks.token);
     29  server.registerPathHandler("/api/v1/fpn/status", callbacks.status);
     30  server.registerPathHandler("/api/v1/fpn/auth", callbacks.enroll);
     31  server.start(-1);
     32  return server;
     33 }
     34 
     35 const testGuardianConfig = server => ({
     36  withToken: async cb => cb("test-token"),
     37  guardianEndpoint: `http://localhost:${server.identity.primaryPort}`,
     38  fxaOrigin: `http://localhost:${server.identity.primaryPort}`,
     39 });
     40 
     41 add_task(async function test_fetchUserInfo() {
     42  const ok = data => {
     43    return (request, r) => {
     44      // Verify the Authorization header is present and correctly formatted
     45      const authHeader = request.getHeader("Authorization");
     46      Assert.ok(authHeader, "Authorization header should be present");
     47      Assert.equal(
     48        authHeader,
     49        "Bearer test-token",
     50        "Authorization header should have the correct format"
     51      );
     52 
     53      r.setStatusLine(request.httpVersion, 200, "OK");
     54      r.write(JSON.stringify(data));
     55    };
     56  };
     57  const fail = status => () => {
     58    throw status;
     59  };
     60  const testcases = [
     61    {
     62      name: "It should parse a valid response",
     63      sends: ok({
     64        subscribed: true,
     65        uid: 42,
     66        created_at: "2023-01-01T12:00:00.000Z",
     67        limited_bandwidth: false,
     68        location_controls: false,
     69        autostart: false,
     70        website_inclusion: false,
     71      }),
     72      expects: {
     73        status: 200,
     74        error: null,
     75        validEntitlement: true,
     76        entitlement: {
     77          subscribed: true,
     78          uid: 42,
     79          created_at: "2023-01-01T12:00:00.000Z",
     80          limited_bandwidth: false,
     81          location_controls: false,
     82          autostart: false,
     83          website_inclusion: false,
     84        },
     85      },
     86    },
     87    {
     88      name: "Alpha experiment",
     89      sends: ok({
     90        autostart: false,
     91        created_at: "2023-09-24T12:00:00.000Z",
     92        limited_bandwidth: false,
     93        location_controls: false,
     94        subscribed: true,
     95        uid: 12345,
     96        website_inclusion: false,
     97        type: "alpha",
     98      }),
     99      expects: {
    100        status: 200,
    101        error: null,
    102        validEntitlement: true,
    103        entitlement: {
    104          autostart: false,
    105          limited_bandwidth: false,
    106          location_controls: false,
    107          subscribed: true,
    108          uid: 12345,
    109          website_inclusion: false,
    110          created_at: "2023-09-24T12:00:00.000Z",
    111        },
    112      },
    113    },
    114    {
    115      name: "Beta experiment",
    116      sends: ok({
    117        autostart: true,
    118        created_at: "2023-09-24T12:30:00.000Z",
    119        limited_bandwidth: false,
    120        location_controls: false,
    121        subscribed: false,
    122        uid: 67890,
    123        website_inclusion: true,
    124        type: "beta",
    125      }),
    126      expects: {
    127        status: 200,
    128        error: null,
    129        validEntitlement: true,
    130        entitlement: {
    131          autostart: true,
    132          limited_bandwidth: false,
    133          location_controls: false,
    134          subscribed: false,
    135          uid: 67890,
    136          website_inclusion: true,
    137          created_at: "2023-09-24T12:30:00.000Z",
    138        },
    139      },
    140    },
    141    {
    142      name: "gamma experiment",
    143      sends: ok({
    144        autostart: true,
    145        created_at: "2023-09-24T13:00:00.000Z",
    146        limited_bandwidth: false,
    147        location_controls: true,
    148        subscribed: true,
    149        uid: 54321,
    150        website_inclusion: false,
    151        type: "gamma",
    152      }),
    153      expects: {
    154        status: 200,
    155        error: null,
    156        validEntitlement: true,
    157        entitlement: {
    158          autostart: true,
    159          limited_bandwidth: false,
    160          location_controls: true,
    161          subscribed: true,
    162          uid: 54321,
    163          website_inclusion: false,
    164          created_at: "2023-09-24T13:00:00.000Z",
    165        },
    166      },
    167    },
    168    {
    169      name: "Delta experiment",
    170      sends: ok({
    171        autostart: true,
    172        created_at: "2023-09-24T13:30:00.000Z",
    173        limited_bandwidth: true,
    174        location_controls: true,
    175        subscribed: true,
    176        uid: 13579,
    177        website_inclusion: true,
    178        type: "delta",
    179      }),
    180      expects: {
    181        status: 200,
    182        error: null,
    183        validEntitlement: true,
    184        entitlement: {
    185          autostart: true,
    186          limited_bandwidth: true,
    187          location_controls: true,
    188          subscribed: true,
    189          uid: 13579,
    190          website_inclusion: true,
    191          created_at: "2023-09-24T13:30:00.000Z",
    192        },
    193      },
    194    },
    195    {
    196      name: "It should handle a 404 response",
    197      sends: fail(HTTP_404),
    198      expects: {
    199        status: 404,
    200        error: "parse_error",
    201        validEntitlement: false,
    202      },
    203    },
    204    {
    205      name: "It should handle an empty response",
    206      sends: ok({}),
    207      expects: {
    208        status: 200,
    209        error: "parse_error",
    210        validEntitlement: false,
    211      },
    212    },
    213    {
    214      name: "It should handle a 200 response with incorrect types",
    215      sends: ok({
    216        subscribed: "true", // Incorrect type: should be boolean
    217        uid: "42", // Incorrect type: should be number
    218        created_at: 1234567890, // Incorrect type: should be string
    219        limited_bandwidth: "false", // Incorrect type: should be boolean
    220        location_controls: "true", // Incorrect type: should be boolean
    221        autostart: "true", // Incorrect type: should be boolean
    222        website_inclusion: "false", // Incorrect type: should be boolean
    223      }),
    224      expects: {
    225        status: 200,
    226        error: "parse_error",
    227        validEntitlement: false, // Should fail validation due to incorrect types
    228      },
    229    },
    230  ];
    231  testcases
    232    .map(({ name, sends, expects }) => {
    233      return async () => {
    234        const server = makeGuardianServer({ status: sends });
    235        const client = new GuardianClient(testGuardianConfig(server));
    236 
    237        const { status, entitlement, error } = await client.fetchUserInfo();
    238 
    239        if (expects.status !== undefined) {
    240          Assert.equal(status, expects.status, `${name}: status should match`);
    241        }
    242 
    243        // Check error message if it's expected
    244        if (expects.error !== null) {
    245          Assert.equal(
    246            error,
    247            expects.error,
    248            `${name}: error should match expected`
    249          );
    250        } else {
    251          Assert.equal(error, undefined, `${name}: error should be undefined`);
    252        }
    253 
    254        if (expects.validEntitlement) {
    255          Assert.notEqual(
    256            entitlement,
    257            null,
    258            `${name}: entitlement should not be null`
    259          );
    260          for (const key of Object.keys(expects.entitlement)) {
    261            // Special case the date case, all others can check equality directly
    262            if (key === "created_at") {
    263              Assert.equal(
    264                new Date(entitlement.created_at).toISOString(),
    265                new Date(
    266                  Date.parse(expects.entitlement.created_at)
    267                ).toISOString(),
    268                `${name}: entitlement.created_at should match`
    269              );
    270            } else {
    271              Assert.equal(
    272                entitlement[key],
    273                expects.entitlement[key],
    274                `${name}: entitlement.${key} should match`
    275              );
    276            }
    277          }
    278        } else {
    279          Assert.equal(
    280            entitlement,
    281            null,
    282            `${name}: entitlement should be null`
    283          );
    284        }
    285 
    286        server.stop();
    287      };
    288    })
    289    .forEach(test => add_task(test));
    290 });
    291 
    292 add_task(async function test_fetchProxyPass() {
    293  const ok = (data, headers = {}) => {
    294    return (request, r) => {
    295      r.setStatusLine(request.httpVersion, 200, "OK");
    296      // Set default Cache-Control header (needed for ProxyPass)
    297      if (!headers["Cache-Control"]) {
    298        r.setHeader("Cache-Control", "max-age=3600", false);
    299      }
    300      // Set any custom headers
    301      for (const [name, value] of Object.entries(headers)) {
    302        r.setHeader(name, value, false);
    303      }
    304      r.write(JSON.stringify(data));
    305    };
    306  };
    307  const fail = status => () => {
    308    throw status;
    309  };
    310  const testcases = [
    311    {
    312      name: "It should parse a valid response",
    313      sends: ok({ token: createProxyPassToken() }),
    314      expects: {
    315        status: 200,
    316        error: null,
    317        validPass: true,
    318      },
    319    },
    320    {
    321      name: "It should handle a 404 response",
    322      sends: fail(HTTP_404),
    323      expects: {
    324        status: 404,
    325        error: "invalid_response",
    326        validPass: false,
    327      },
    328    },
    329    {
    330      name: "It should handle an empty response",
    331      sends: ok({}),
    332      expects: {
    333        status: 200,
    334        error: "invalid_response",
    335        validPass: false,
    336      },
    337    },
    338    {
    339      name: "It should handle an invalid token format",
    340      sends: ok({ token: "header.body.signature" }),
    341      expects: {
    342        status: 200,
    343        error: "invalid_response",
    344        validPass: false,
    345      },
    346    },
    347  ];
    348  testcases
    349    .map(({ name, sends, expects }) => {
    350      return async () => {
    351        const server = makeGuardianServer({ token: sends });
    352        const client = new GuardianClient(testGuardianConfig(server));
    353 
    354        const { status, pass, error } = await client.fetchProxyPass();
    355 
    356        if (expects.status !== undefined) {
    357          Assert.equal(status, expects.status, `${name}: status should match`);
    358        }
    359 
    360        // Check error message if it's expected
    361        if (expects.error !== null) {
    362          Assert.equal(
    363            error,
    364            expects.error,
    365            `${name}: error should match expected`
    366          );
    367        } else {
    368          Assert.equal(error, undefined, `${name}: error should be undefined`);
    369        }
    370 
    371        if (expects.validPass) {
    372          Assert.notEqual(pass, null, `${name}: pass should not be null`);
    373          Assert.strictEqual(
    374            typeof pass.token,
    375            "string",
    376            `${name}: pass.token should be a string`
    377          );
    378          Assert.greater(
    379            pass.until.epochMilliseconds,
    380            Date.now(),
    381            `${name}: pass.until should be in the future`
    382          );
    383          Assert.ok(pass.isValid(), `${name}: pass should be valid`);
    384        } else {
    385          Assert.equal(pass, null, `${name}: pass should be null`);
    386        }
    387 
    388        server.stop();
    389      };
    390    })
    391    .forEach(test => add_task(test));
    392 });
    393 
    394 add_task(async function test_parseGuardianSuccessURL() {
    395  const testcases = [
    396    {
    397      name: "Valid success URL with code",
    398      input: "https://example.com/oauth/success?code=abc123",
    399      expects: { ok: true, error: undefined },
    400    },
    401    {
    402      name: "Error in URL",
    403      input: "https://example.com/oauth/success?error=generic_error",
    404      expects: { ok: false, error: "generic_error" },
    405    },
    406    {
    407      name: "Missing code in success URL",
    408      input: "https://example.com/oauth/success",
    409      expects: { ok: false, error: "missing_code" },
    410    },
    411    {
    412      name: "Null input",
    413      input: null,
    414      expects: { ok: false, error: "timeout" },
    415    },
    416  ];
    417 
    418  testcases.forEach(({ name, input, expects }) => {
    419    info(`Running test case: ${name}`);
    420 
    421    const result = GuardianClient._parseGuardianSuccessURL(input);
    422 
    423    Assert.equal(result.ok, expects.ok, `${name}: ok should match`);
    424    Assert.equal(result.error, expects.error, `${name}: error should match`);
    425  });
    426 });
    427 
    428 add_task(async function test_proxyPassShouldRotate() {
    429  const oneHour = Temporal.Duration.from({ hours: 1 });
    430  const from = Temporal.Instant.from("2025-12-08T12:00:00Z"); // Static point in time
    431  // The pass is valid for 1 hour from 'from'
    432  const until = from.add(oneHour);
    433  const rotationTime = ProxyPass.ROTATION_TIME;
    434 
    435  const testcases = [
    436    {
    437      name: "Should not rotate when before rotation time",
    438      currentTime: until.subtract(rotationTime).subtract({ seconds: 1 }),
    439      expects: { shouldRotate: false },
    440    },
    441    {
    442      name: "Should rotate when at rotation time",
    443      currentTime: until.subtract(rotationTime),
    444      expects: { shouldRotate: true },
    445    },
    446    {
    447      name: "Should rotate when after rotation time",
    448      currentTime: until.subtract(rotationTime).add({ seconds: 1 }),
    449      expects: { shouldRotate: true },
    450    },
    451    {
    452      name: "Should rotate when pass is expired",
    453      currentTime: until.add({ seconds: 1 }),
    454      expects: { shouldRotate: true },
    455    },
    456  ];
    457 
    458  testcases.forEach(({ name, currentTime, expects }) => {
    459    info(`Running test case: ${name}`);
    460    const proxyPass = new ProxyPass(createProxyPassToken(from, until));
    461    const result = proxyPass.shouldRotate(currentTime);
    462    Assert.equal(
    463      result,
    464      expects.shouldRotate,
    465      `${name}: shouldRotate should match`
    466    );
    467  });
    468 });