Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  test_remote_settings.js   Sprache: JAVA

 
/* import-globals-from ../../../common/tests/unit/head_helpers.js */

const { ObjectUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/ObjectUtils.sys.mjs"
);

const IS_ANDROID = AppConstants.platform == "android";

const TELEMETRY_COMPONENT = "remotesettings";
const TELEMETRY_EVENTS_FILTERS = {
  category: "uptake.remotecontent.result",
  method: "uptake",
};

let server;
let client;
let clientWithDump;

async function clear_state() {
  // Reset preview mode.
  RemoteSettings.enablePreviewMode(undefined);
  Services.prefs.clearUserPref("services.settings.preview_enabled");

  client.verifySignature = false;
  clientWithDump.verifySignature = false;

  // Clear local DB.
  await client.db.clear();
  // Reset event listeners.
  client._listeners.set("sync", []);

  await clientWithDump.db.clear();

  // Clear events snapshot.
  TelemetryTestUtils.assertEvents([], {}, { process: "dummy" });
}

add_task(() => {
  // Set up an HTTP Server
  server = new HttpServer();
  server.start(-1);

  // Pretend we are in nightly channel to make sure all telemetry events are sent.
  let oldGetChannel = Policy.getChannel;
  Policy.getChannel = () => "nightly";

  // Point the blocklist clients to use this local HTTP server.
  Services.prefs.setStringPref(
    "services.settings.server",
    `http://localhost:${server.identity.primaryPort}/v1`
  );

  Services.prefs.setStringPref("services.settings.loglevel""debug");

  client = RemoteSettings("password-fields");
  clientWithDump = RemoteSettings("language-dictionaries");

  server.registerPathHandler("/v1/", handleResponse);
  server.registerPathHandler(
    "/v1/buckets/monitor/collections/changes/changeset",
    handleResponse
  );
  server.registerPathHandler(
    "/v1/buckets/main/collections/password-fields/changeset",
    handleResponse
  );
  server.registerPathHandler(
    "/v1/buckets/main/collections/language-dictionaries/changeset",
    handleResponse
  );
  server.registerPathHandler(
    "/v1/buckets/main/collections/with-local-fields/changeset",
    handleResponse
  );
  server.registerPathHandler("/fake-x5u", handleResponse);

  registerCleanupFunction(() => {
    Policy.getChannel = oldGetChannel;
    server.stop(() => {});
  });
});
add_task(clear_state);

add_task(async function test_records_obtained_from_server_are_stored_in_db() {
  // Test an empty db populates
  await client.maybeSync(2000);

  // Open the collection, verify it's been populated:
  // Our test data has a single record; it should be in the local collection
  const list = await client.get();
  equal(list.length, 1);

  const timestamp = await client.db.getLastModified();
  equal(timestamp, 3000, "timestamp was stored");

  const { signature } = await client.db.getMetadata();
  equal(signature.signature, "abcdef""metadata was stored");
});
add_task(clear_state);

add_task(
  async function test_records_from_dump_are_listed_as_created_in_event() {
    if (IS_ANDROID) {
      // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
      return;
    }
    let received;
    clientWithDump.on("sync", ({ data }) => (received = data));
    // Use a timestamp superior to latest record in dump.
    const timestamp = 5000000000000; // Fri Jun 11 2128

    await clientWithDump.maybeSync(timestamp);

    const list = await clientWithDump.get();
    Assert.greater(
      list.length,
      20,
      `The dump was loaded (${list.length} records)`
    );
    equal(received.created[0].id, "xx""Record from the sync come first.");

    const createdById = received.created.reduce((acc, r) => {
      acc[r.id] = r;
      return acc;
    }, {});

    ok(
      !(received.deleted[0].id in createdById),
      "Deleted records are not listed as created"
    );
    equal(
      createdById[received.updated[0].new.id],
      received.updated[0].new,
      "The records that were updated should appear as created in their newest form."
    );

    equal(
      received.created.length,
      list.length,
      "The list of created records contains the dump"
    );
    equal(received.current.length, received.created.length);
  }
);
add_task(clear_state);

add_task(async function test_throws_when_network_is_offline() {
  const backupOffline = Services.io.offline;
  try {
    Services.io.offline = true;
    const startSnapshot = getUptakeTelemetrySnapshot(
      TELEMETRY_COMPONENT,
      clientWithDump.identifier
    );
    let error;
    try {
      await clientWithDump.maybeSync(2000);
    } catch (e) {
      error = e;
    }
    equal(error.name, "NetworkOfflineError");

    const endSnapshot = getUptakeTelemetrySnapshot(
      TELEMETRY_COMPONENT,
      clientWithDump.identifier
    );
    const expectedIncrements = {
      [UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR]: 1,
    };
    checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
  } finally {
    Services.io.offline = backupOffline;
  }
});
add_task(clear_state);

add_task(async function test_sync_event_is_sent_even_if_up_to_date() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }
  // First, determine what is the dump timestamp. Sync will load it.
  // Use a timestamp inferior to latest record in dump.
  await clientWithDump._importJSONDump();
  const uptodateTimestamp = await clientWithDump.db.getLastModified();
  await clear_state();

  // Now, simulate that server data wasn't changed since dump was released.
  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    clientWithDump.identifier
  );
  let received;
  clientWithDump.on("sync", ({ data }) => (received = data));

  await clientWithDump.maybeSync(uptodateTimestamp);

  ok(!!received.current.length, "Dump records are listed as created");
  equal(received.current.length, received.created.length);

  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    clientWithDump.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.UP_TO_DATE]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_records_can_have_local_fields() {
  const c = RemoteSettings("with-local-fields", { localFields: ["accepted"] });
  c.verifySignature = false;

  await c.maybeSync(2000);

  await c.db.update({
    id: "c74279ce-fb0a-42a6-ae11-386b567a6119",
    accepted: true,
  });
  await c.maybeSync(3000); // Does not fail.
});
add_task(clear_state);

add_task(
  async function test_records_changes_are_overwritten_by_server_changes() {
    // Create some local conflicting data, and make sure it syncs without error.
    await client.db.create({
      website: "",
      id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
    });

    await client.maybeSync(2000);

    const data = await client.get();
    equal(data[0].website, "https://some-website.com");
  }
);
add_task(clear_state);

add_task(
  async function test_get_loads_default_records_from_a_local_dump_when_database_is_empty() {
    if (IS_ANDROID) {
      // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
      return;
    }

    // When collection has a dump in services/settings/dumps/{bucket}/{collection}.json
    const data = await clientWithDump.get();
    notEqual(data.length, 0);
    // No synchronization happened (responses are not mocked).
  }
);
add_task(clear_state);

add_task(async function test_get_loads_dump_only_once_if_called_in_parallel() {
  const backup = clientWithDump._importJSONDump;
  let callCount = 0;
  clientWithDump._importJSONDump = async () => {
    callCount++;
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(resolve => setTimeout(resolve, 100));
    return 42;
  };
  await Promise.all([clientWithDump.get(), clientWithDump.get()]);
  equal(callCount, 1, "JSON dump was called more than once");
  clientWithDump._importJSONDump = backup;
});
add_task(clear_state);

add_task(async function test_get_falls_back_to_dump_if_db_fails() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }
  const backup = clientWithDump.db.getLastModified;
  clientWithDump.db.getLastModified = () => {
    throw new Error("Unknown error");
  };

  const records = await clientWithDump.get({ dumpFallback: true });
  ok(!!records.length, "dump content is returned");

  // If fallback is disabled, error is thrown.
  let error;
  try {
    await clientWithDump.get({ dumpFallback: false });
  } catch (e) {
    error = e;
  }
  equal(error.message, "Unknown error");

  clientWithDump.db.getLastModified = backup;
});
add_task(clear_state);

add_task(async function test_get_sorts_results_if_specified() {
  await client.db.importChanges(
    {},
    42,
    [
      {
        field: 12,
        id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
      },
      {
        field: 7,
        id: "d83444a4-f348-4cd8-8228-842cb927db9f",
      },
    ],
    { clear: true }
  );

  const records = await client.get({ order: "field" });
  Assert.less(
    records[0].field,
    records[records.length - 1].field,
    "records are sorted"
  );
});
add_task(clear_state);

add_task(async function test_get_falls_back_sorts_results() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }
  const backup = clientWithDump.db.getLastModified;
  clientWithDump.db.getLastModified = () => {
    throw new Error("Unknown error");
  };

  const records = await clientWithDump.get({
    dumpFallback: true,
    order: "-id",
  });

  // eslint-disable-next-line mozilla/no-comparison-or-assignment-inside-ok
  ok(records[0].id > records[records.length - 1].id, "records are sorted");

  clientWithDump.db.getLastModified = backup;
});
add_task(clear_state);

add_task(async function test_get_falls_back_to_dump_if_db_fails_later() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }
  const backup = clientWithDump.db.list;
  clientWithDump.db.list = () => {
    throw new Error("Unknown error");
  };

  const records = await clientWithDump.get({ dumpFallback: true });
  ok(!!records.length, "dump content is returned");

  // If fallback is disabled, error is thrown.
  let error;
  try {
    await clientWithDump.get({ dumpFallback: false });
  } catch (e) {
    error = e;
  }
  equal(error.message, "Unknown error");

  clientWithDump.db.list = backup;
});
add_task(clear_state);

add_task(async function test_get_falls_back_to_dump_if_network_fails() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }
  const backup = clientWithDump.sync;
  clientWithDump.sync = () => {
    throw new Error("Sync error");
  };

  const records = await clientWithDump.get();
  ok(!!records.length, "dump content is returned");

  clientWithDump.sync = backup;
});
add_task(clear_state);

add_task(async function test_get_does_not_sync_if_empty_dump_is_provided() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }

  const clientWithEmptyDump = RemoteSettings("example");
  Assert.ok(!(await Utils.hasLocalData(clientWithEmptyDump)));

  const data = await clientWithEmptyDump.get();

  equal(data.length, 0);
  Assert.ok(await Utils.hasLocalData(clientWithEmptyDump));
});
add_task(clear_state);

add_task(async function test_get_synchronization_can_be_disabled() {
  const data = await client.get({ syncIfEmpty: false });

  equal(data.length, 0);
});
add_task(clear_state);

add_task(
  async function test_get_triggers_synchronization_when_database_is_empty() {
    // The "password-fields" collection has no local dump, and no local data.
    // Therefore a synchronization will happen.
    const data = await client.get();

    // Data comes from mocked HTTP response (see below).
    equal(data.length, 1);
    equal(data[0].selector, "#webpage[field-pwd]");
  }
);
add_task(clear_state);

add_task(async function test_get_ignores_synchronization_errors_by_default() {
  // The monitor endpoint won't contain any information about this collection.
  let data = await RemoteSettings("some-unknown-key").get();
  equal(data.length, 0);
  // The sync endpoints are not mocked, this fails internally.
  data = await RemoteSettings("no-mocked-responses").get();
  equal(data.length, 0);
});
add_task(clear_state);

add_task(async function test_get_throws_if_no_empty_fallback() {
  // The monitor endpoint won't contain any information about this collection.
  try {
    await RemoteSettings("some-unknown-key").get({
      emptyListFallback: false,
    });
    Assert.ok(false".get() should throw");
  } catch (error) {
    Assert.ok(
      error.message.includes("Response from server unparseable"),
      "Server error was thrown"
    );
  }
});
add_task(clear_state);

add_task(async function test_get_verify_signature_no_sync() {
  // No signature in metadata, and no sync if empty.
  let error;
  try {
    await client.get({ verifySignature: true, syncIfEmpty: false });
  } catch (e) {
    error = e;
  }
  equal(error.message, "Missing signature (main/password-fields)");
});
add_task(clear_state);

add_task(async function test_get_can_verify_signature_pulled() {
  // Populate the local DB (only records, eg. loaded from dump previously)
  await client._importJSONDump();

  let calledSignature;
  client._verifier = {
    async asyncVerifyContentSignature(serialized, signature) {
      calledSignature = signature;
      return true;
    },
  };
  client.verifySignature = true;

  // No metadata in local DB, but gets pulled and then verifies.
  ok(ObjectUtils.isEmpty(await client.db.getMetadata()), "Metadata is empty");

  await client.get({ verifySignature: true });

  ok(
    !ObjectUtils.isEmpty(await client.db.getMetadata()),
    "Metadata was pulled"
  );
  ok(calledSignature.endsWith("some-sig"), "Signature was verified");
});
add_task(clear_state);

add_task(async function test_get_can_verify_signature() {
  // Populate the local DB (record and metadata)
  await client.maybeSync(2000);

  // It validates signature that was stored in local DB.
  let calledSignature;
  client._verifier = {
    async asyncVerifyContentSignature(serialized, signature) {
      calledSignature = signature;
      return JSON.parse(serialized).data.length == 1;
    },
  };
  ok(await Utils.hasLocalData(client), "Local data was populated");
  await client.get({ verifySignature: true });

  ok(calledSignature.endsWith("abcdef"), "Signature was verified");

  // It throws when signature does not verify.
  await client.db.delete("9d500963-d80e-3a91-6e74-66f3811b99cc");
  let error = null;
  try {
    await client.get({ verifySignature: true });
  } catch (e) {
    error = e;
  }
  equal(
    error.message,
    "Invalid content signature (main/password-fields) using 'fake-x5u' and signer remote-settings.content-signature.mozilla.org"
  );
});
add_task(clear_state);

add_task(async function test_get_does_not_verify_signature_if_load_dump() {
  if (IS_ANDROID) {
    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
    return;
  }

  let called;
  clientWithDump._verifier = {
    async asyncVerifyContentSignature() {
      called = true;
      return true;
    },
  };

  // When dump is loaded, signature is not verified.
  const records = await clientWithDump.get({ verifySignature: true });
  ok(!!records.length, "dump is loaded");
  ok(!called, "signature is missing but not verified");

  // If metadata is missing locally, it is not fetched if `syncIfEmpty` is disabled.
  let error;
  try {
    await clientWithDump.get({ verifySignature: true, syncIfEmpty: false });
  } catch (e) {
    error = e;
  }
  ok(!called, "signer was not called");
  equal(
    error.message,
    "Missing signature (main/language-dictionaries)",
    "signature is missing locally"
  );

  // If metadata is missing locally, it is fetched by default (`syncIfEmpty: true`)
  await clientWithDump.get({ verifySignature: true });
  const metadata = await clientWithDump.db.getMetadata();
  ok(!!Object.keys(metadata).length, "metadata was fetched");
  ok(called, "signature was verified for the data that was in dump");
});
add_task(clear_state);

add_task(
  async function test_get_does_verify_signature_if_json_loaded_in_parallel() {
    const backup = clientWithDump._verifier;
    let callCount = 0;
    clientWithDump._verifier = {
      async asyncVerifyContentSignature() {
        callCount++;
        return true;
      },
    };
    await Promise.all([
      clientWithDump.get({ verifySignature: true }),
      clientWithDump.get({ verifySignature: true }),
    ]);
    equal(callCount, 0, "No need to verify signatures if JSON dump is loaded");
    clientWithDump._verifier = backup;
  }
);
add_task(clear_state);

add_task(async function test_get_can_force_a_sync() {
  const step0 = await client.db.getLastModified();
  await client.get({ forceSync: true });
  const step1 = await client.db.getLastModified();
  await client.get();
  const step2 = await client.db.getLastModified();
  await client.get({ forceSync: true });
  const step3 = await client.db.getLastModified();

  equal(step0, null);
  equal(step1, 3000);
  equal(step2, 3000);
  equal(step3, 3001);
});
add_task(clear_state);

add_task(async function test_sync_runs_once_only() {
  const backup = Utils.log.warn;
  const messages = [];
  Utils.log.warn = m => {
    messages.push(m);
  };

  await Promise.all([client.maybeSync(2000), client.maybeSync(2000)]);

  ok(
    messages.includes("main/password-fields sync already running"),
    "warning is shown about sync already running"
  );
  Utils.log.warn = backup;
});
add_task(clear_state);

add_task(
  async function test_sync_pulls_metadata_if_missing_with_dump_is_up_to_date() {
    if (IS_ANDROID) {
      // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
      return;
    }

    let called;
    clientWithDump._verifier = {
      async asyncVerifyContentSignature() {
        called = true;
        return true;
      },
    };
    // When dump is loaded, signature is not verified.
    const records = await clientWithDump.get({ verifySignature: true });
    ok(!!records.length, "dump is loaded");
    ok(!called, "signature is missing but not verified");

    // Synchronize the collection (local data is up-to-date).
    // Signature verification is disabled (see `clear_state()`), so we don't bother with
    // fetching metadata.
    const uptodateTimestamp = await clientWithDump.db.getLastModified();
    await clientWithDump.maybeSync(uptodateTimestamp);
    let metadata = await clientWithDump.db.getMetadata();
    ok(!metadata, "metadata was not fetched");

    // Synchronize again the collection (up-to-date, since collection last modified still > 42)
    clientWithDump.verifySignature = true;
    await clientWithDump.maybeSync(42);

    // With signature verification, metadata was fetched.
    metadata = await clientWithDump.db.getMetadata();
    ok(!!Object.keys(metadata).length, "metadata was fetched");
    ok(called, "signature was verified for the data that was in dump");

    // Metadata is present, signature will now verified.
    called = false;
    await clientWithDump.get({ verifySignature: true });
    ok(called, "local signature is verified");
  }
);
add_task(clear_state);

add_task(async function test_sync_event_provides_information_about_records() {
  let eventData;
  client.on("sync", ({ data }) => (eventData = data));

  await client.maybeSync(2000);
  equal(eventData.current.length, 1);

  await client.maybeSync(3001);
  equal(eventData.current.length, 2);
  equal(eventData.created.length, 1);
  equal(eventData.created[0].website, "https://www.other.org/signin");
  equal(eventData.updated.length, 1);
  equal(eventData.updated[0].old.website, "https://some-website.com");
  equal(eventData.updated[0].new.website, "https://some-website.com/login");
  equal(eventData.deleted.length, 0);

  await client.maybeSync(4001);
  equal(eventData.current.length, 1);
  equal(eventData.created.length, 0);
  equal(eventData.updated.length, 0);
  equal(eventData.deleted.length, 1);
  equal(eventData.deleted[0].website, "https://www.other.org/signin");
});
add_task(clear_state);

add_task(async function test_inspect_method() {
  // Synchronize the `password-fields` collection in order to have
  // some local data when .inspect() is called.
  await client.maybeSync(2000);

  const inspected = await RemoteSettings.inspect();

  // Assertion for global attributes.
  const { mainBucket, serverURL, defaultSigner, collections, serverTimestamp } =
    inspected;
  const rsSigner = "remote-settings.content-signature.mozilla.org";
  equal(mainBucket, "main");
  equal(serverURL, `http://localhost:${server.identity.primaryPort}/v1`);
  equal(defaultSigner, rsSigner);
  equal(serverTimestamp, '"5000"');

  // A collection is listed in .inspect() if it has local data or if there
  // is a JSON dump for it.
  // "password-fields" has no dump but was synchronized above and thus has local data.
  let col = collections.pop();
  equal(col.collection, "password-fields");
  equal(col.serverTimestamp, 3000);
  equal(col.localTimestamp, 3000);

  if (!IS_ANDROID) {
    // "language-dictionaries" has a local dump (not on Android)
    col = collections.pop();
    equal(col.collection, "language-dictionaries");
    equal(col.serverTimestamp, 4000);
    ok(!col.localTimestamp); // not synchronized.
  }
});
add_task(clear_state);

add_task(async function test_inspect_method_uses_a_random_cache_bust() {
  const backup = Utils.fetchLatestChanges;
  const cacheBusts = [];
  Utils.fetchLatestChanges = (url, options) => {
    cacheBusts.push(options.expected);
    return { changes: [] };
  };

  await RemoteSettings.inspect();
  await RemoteSettings.inspect();
  await RemoteSettings.inspect();

  notEqual(cacheBusts[0], cacheBusts[1]);
  notEqual(cacheBusts[1], cacheBusts[2]);
  notEqual(cacheBusts[0], cacheBusts[2]);
  Utils.fetchLatestChanges = backup;
});

add_task(async function test_clearAll_method() {
  // Make sure we have some local data.
  await client.maybeSync(2000);
  await clientWithDump.maybeSync(2000);

  await RemoteSettings.clearAll();

  ok(!(await Utils.hasLocalData(client)), "Local data was deleted");
  ok(!(await Utils.hasLocalData(clientWithDump)), "Local data was deleted");
  ok(
    !Services.prefs.prefHasUserValue(client.lastCheckTimePref),
    "Pref was cleaned"
  );

  // Synchronization is not broken after resuming.
  await client.maybeSync(2000);
  await clientWithDump.maybeSync(2000);
  ok(await Utils.hasLocalData(client), "Local data was populated");
  ok(await Utils.hasLocalData(clientWithDump), "Local data was populated");
});
add_task(clear_state);

add_task(async function test_listeners_are_not_deduplicated() {
  let count = 0;
  const plus1 = () => {
    count += 1;
  };

  client.on("sync", plus1);
  client.on("sync", plus1);
  client.on("sync", plus1);

  await client.maybeSync(2000);

  equal(count, 3);
});
add_task(clear_state);

add_task(async function test_listeners_can_be_removed() {
  let count = 0;
  const onSync = () => {
    count += 1;
  };

  client.on("sync", onSync);
  client.off("sync", onSync);

  await client.maybeSync(2000);

  equal(count, 0);
});
add_task(clear_state);

add_task(async function test_all_listeners_are_executed_if_one_fails() {
  let count = 0;
  client.on("sync", () => {
    count += 1;
  });
  client.on("sync", () => {
    throw new Error("boom");
  });
  client.on("sync", () => {
    count += 2;
  });

  let error;
  try {
    await client.maybeSync(2000);
  } catch (e) {
    error = e;
  }

  equal(count, 3);
  equal(error.message, "boom");
});
add_task(clear_state);

add_task(async function test_telemetry_reports_up_to_date() {
  await client.maybeSync(2000);
  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  await client.maybeSync(3000);

  // No Telemetry was sent.
  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.UP_TO_DATE]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_if_sync_succeeds() {
  // We test each client because Telemetry requires preleminary declarations.
  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  await client.maybeSync(2000);

  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.SUCCESS]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(
  async function test_synchronization_duration_is_reported_in_uptake_status() {
    await client.maybeSync(2000);

    TelemetryTestUtils.assertEvents(
      [
        [
          "uptake.remotecontent.result",
          "uptake",
          "remotesettings",
          UptakeTelemetry.STATUS.SUCCESS,
          {
            source: client.identifier,
            duration: v => v > 0,
            trigger: "manual",
          },
        ],
      ],
      TELEMETRY_EVENTS_FILTERS
    );
  }
);
add_task(clear_state);

add_task(async function test_telemetry_reports_if_application_fails() {
  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  client.on("sync", () => {
    throw new Error("boom");
  });

  try {
    await client.maybeSync(2000);
  } catch (e) {}

  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.APPLY_ERROR]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_reports_if_sync_fails() {
  await client.db.importChanges({}, 9999);

  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  try {
    await client.maybeSync(10000);
  } catch (e) {}

  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.SERVER_ERROR]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_reports_if_parsing_fails() {
  await client.db.importChanges({}, 10000);

  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  try {
    await client.maybeSync(10001);
  } catch (e) {}

  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.PARSE_ERROR]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_reports_if_fetching_signature_fails() {
  await client.db.importChanges({}, 11000);

  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  try {
    await client.maybeSync(11001);
  } catch (e) {}

  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.SERVER_ERROR]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_reports_unknown_errors() {
  const backup = client.db.list;
  client.db.list = () => {
    throw new Error("Internal");
  };
  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  try {
    await client.maybeSync(2000);
  } catch (e) {}

  client.db.list = backup;
  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_reports_indexeddb_as_custom_1() {
  const backup = client.db.getLastModified;
  const msg =
    "IndexedDB getLastModified() The operation failed for reasons unrelated to the database itself";
  client.db.getLastModified = () => {
    throw new Error(msg);
  };
  const startSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );

  try {
    await client.maybeSync(2000);
  } catch (e) {}

  client.db.getLastModified = backup;
  const endSnapshot = getUptakeTelemetrySnapshot(
    TELEMETRY_COMPONENT,
    client.identifier
  );
  const expectedIncrements = { [UptakeTelemetry.STATUS.CUSTOM_1_ERROR]: 1 };
  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
});
add_task(clear_state);

add_task(async function test_telemetry_reports_error_name_as_event_nightly() {
  const backup = client.db.list;
  client.db.list = () => {
    const e = new Error("Some unknown error");
    e.name = "ThrownError";
    throw e;
  };

  try {
    await client.maybeSync(2000);
  } catch (e) {}

  TelemetryTestUtils.assertEvents(
    [
      [
        "uptake.remotecontent.result",
        "uptake",
        "remotesettings",
        UptakeTelemetry.STATUS.UNKNOWN_ERROR,
        {
          source: client.identifier,
          trigger: "manual",
          duration: v => v >= 0,
          errorName: "ThrownError",
        },
      ],
    ],
    TELEMETRY_EVENTS_FILTERS
  );

  client.db.list = backup;
});
add_task(clear_state);

add_task(async function test_bucketname_changes_when_preview_mode_is_enabled() {
  equal(client.bucketName, "main");

  RemoteSettings.enablePreviewMode(true);

  equal(client.bucketName, "main-preview");
});
add_task(clear_state);

add_task(
  async function test_preview_mode_pref_affects_bucket_names_before_instantiated() {
    Services.prefs.setBoolPref("services.settings.preview_enabled"true);

    let clientWithDefaultBucket = RemoteSettings("other");
    let clientWithBucket = RemoteSettings("coll", { bucketName: "buck" });

    equal(clientWithDefaultBucket.bucketName, "main-preview");
    equal(clientWithBucket.bucketName, "buck-preview");
  }
);
add_task(clear_state);

add_task(
  async function test_preview_enabled_pref_ignored_when_mode_is_set_explicitly() {
    Services.prefs.setBoolPref("services.settings.preview_enabled"true);

    let clientWithDefaultBucket = RemoteSettings("other");
    let clientWithBucket = RemoteSettings("coll", { bucketName: "buck" });

    equal(clientWithDefaultBucket.bucketName, "main-preview");
    equal(clientWithBucket.bucketName, "buck-preview");

    RemoteSettings.enablePreviewMode(false);

    equal(clientWithDefaultBucket.bucketName, "main");
    equal(clientWithBucket.bucketName, "buck");
  }
);
add_task(clear_state);

add_task(
  async function test_get_loads_default_records_from_a_local_dump_when_preview_mode_is_enabled() {
    if (IS_ANDROID) {
      // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
      return;
    }
    RemoteSettings.enablePreviewMode(true);
    // When collection has a dump in services/settings/dumps/{bucket}/{collection}.json
    const data = await clientWithDump.get();
    notEqual(data.length, 0);
    // No synchronization happened (responses are not mocked).
  }
);
add_task(clear_state);

add_task(async function test_local_db_distinguishes_preview_records() {
  RemoteSettings.enablePreviewMode(true);
  client.db.importChanges({}, Date.now(), [{ id: "record-1" }], {
    clear: true,
  });

  RemoteSettings.enablePreviewMode(false);
  client.db.importChanges({}, Date.now(), [{ id: "record-2" }], {
    clear: true,
  });

  deepEqual(await client.get(), [{ id: "record-2" }]);
});
add_task(clear_state);

add_task(
  async function test_inspect_changes_the_list_when_preview_mode_is_enabled() {
    if (IS_ANDROID) {
      // Skip test: we don't ship remote settings dumps on Android (see package-manifest),
      // and this test relies on the fact that clients are instantiated if a dump is packaged.
      return;
    }

    // Register a client only listed in -preview...
    RemoteSettings("crash-rate");

    const { collections: before, previewMode: previewModeBefore } =
      await RemoteSettings.inspect();

    Assert.ok(!previewModeBefore, "preview is not enabled");

    // These two collections are listed in the main bucket in monitor/changes (one with dump, one registered).
    deepEqual(before.map(c => c.collection).sort(), [
      "language-dictionaries",
      "password-fields",
    ]);

    // Switch to preview mode.
    RemoteSettings.enablePreviewMode(true);

    const {
      collections: after,
      mainBucket,
      previewMode,
    } = await RemoteSettings.inspect();

    Assert.ok(previewMode, "preview is enabled");

    // These two collections are listed in the main bucket in monitor/changes (both are registered).
    deepEqual(after.map(c => c.collection).sort(), [
      "crash-rate",
      "password-fields",
    ]);
    equal(mainBucket, "main-preview");
  }
);
add_task(clear_state);

add_task(async function test_sync_event_is_not_sent_from_get_when_no_dump() {
  let called = false;
  client.on("sync", () => {
    called = true;
  });

  await client.get();

  Assert.ok(!called, "sync event is not sent from .get()");
});
add_task(clear_state);

add_task(async function test_get_can_be_called_from_sync_event_callback() {
  let fromGet;
  let fromEvent;

  client.on("sync", async ({ data: { current } }) => {
    // Before fixing Bug 1761953 this would result in a deadlock.
    fromGet = await client.get();
    fromEvent = current;
  });

  await client.maybeSync(2000);

  Assert.ok(fromGet, "sync callback was called");
  Assert.deepEqual(fromGet, fromEvent, ".get() gives current records list");
});
add_task(clear_state);

add_task(async function test_attachments_are_pruned_when_sync_from_timer() {
  await client.db.saveAttachment("bar", {
    record: { id: "bar" },
    blob: new Blob(["456"]),
  });

  await client.maybeSync(2000, { trigger: "broadcast" });

  Assert.ok(
    await client.attachments.cacheImpl.get("bar"),
    "Extra attachment was not deleted on broadcast"
  );

  await client.maybeSync(3001, { trigger: "timer" });

  Assert.ok(
    !(await client.attachments.cacheImpl.get("bar")),
    "Extra attachment was deleted on timer"
  );
});
add_task(clear_state);

function handleResponse(request, response) {
  try {
    const sample = getSampleResponse(request, server.identity.primaryPort);
    if (!sample) {
      do_throw(
        `unexpected ${request.method} request for ${request.path}?${request.queryString}`
      );
    }

    response.setStatusLine(
      null,
      sample.status.status,
      sample.status.statusText
    );
    // send the headers
    for (let headerLine of sample.sampleHeaders) {
      let headerElements = headerLine.split(":");
      response.setHeader(headerElements[0], headerElements[1].trimLeft());
    }
    response.setHeader("Date"new Date().toUTCString());

    const body =
      typeof sample.responseBody == "string"
        ? sample.responseBody
        : JSON.stringify(sample.responseBody);
    response.write(body);
    response.finish();
  } catch (e) {
    info(e);
  }
}

function getSampleResponse(req, port) {
  const responses = {
    OPTIONS: {
      sampleHeaders: [
        "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
        "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
        "Access-Control-Allow-Origin: *",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
      ],
      status: { status: 200, statusText: "OK" },
      responseBody: null,
    },
    "GET:/v1/": {
      sampleHeaders: [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
      ],
      status: { status: 200, statusText: "OK" },
      responseBody: {
        settings: {
          batch_max_requests: 25,
        },
        url: `http://localhost:${port}/v1/`,
        documentation: "https://kinto.readthedocs.org/",
        version: "1.5.1",
        commit: "cbc6f58",
        hello: "kinto",
      },
    },
    "GET:/v1/buckets/monitor/collections/changes/changeset": {
      sampleHeaders: [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
        `Date: ${new Date().toUTCString()}`,
        'Etag: "5000"',
      ],
      status: { status: 200, statusText: "OK" },
      responseBody: {
        timestamp: 5000,
        changes: [
          {
            id: "4676f0c7-9757-4796-a0e8-b40a5a37a9c9",
            bucket: "main",
            collection: "unknown-locally",
            last_modified: 5000,
          },
          {
            id: "4676f0c7-9757-4796-a0e8-b40a5a37a9c9",
            bucket: "main",
            collection: "language-dictionaries",
            last_modified: 4000,
          },
          {
            id: "0af8da0b-3e03-48fb-8d0d-2d8e4cb7514d",
            bucket: "main",
            collection: "password-fields",
            last_modified: 3000,
          },
          {
            id: "4acda969-3bd3-4074-a678-ff311eeb076e",
            bucket: "main-preview",
            collection: "password-fields",
            last_modified: 2000,
          },
          {
            id: "58697bd1-315f-4185-9bee-3371befc2585",
            bucket: "main-preview",
            collection: "crash-rate",
            last_modified: 1000,
          },
        ],
      },
    },
    "GET:/fake-x5u": {
      sampleHeaders: ["Content-Type: application/octet-stream"],
      status: { status: 200, statusText: "OK" },
      responseBody: `-----BEGIN CERTIFICATE-----
MIIGYTCCBEmgAwIBAgIBATANBgkqhkiG9w0BAQwFADB9MQswCQYDVQQGEwJVU
ZARKjbu1TuYQHf0fs+GwID8zeLc2zJL7UzcHFwwQ6Nda9OJN4uPAuC/BKaIpxCLL
26b24/tRam4SJjqpiq20lynhUrmTtt6hbG3E1Hpy3bmkt2DYnuMFwEx2gfXNcnbT
wNuvFqc=
-----END CERTIFICATE-----`,
    },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=2000":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "3000"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          timestamp: 3000,
          metadata: {
            id: "password-fields",
            last_modified: 1234,
            signature: {
              signature: "abcdef",
              x5u: `http://localhost:${port}/fake-x5u`,
            },
          },
          changes: [
            {
              id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
              last_modified: 3000,
              website: "https://some-website.com",
              selector: "#user[password]",
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=%223000%22":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "4000"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          metadata: {
            signature: {},
          },
          timestamp: 4000,
          changes: [
            {
              id: "aabad965-e556-ffe7-4191-074f5dee3df3",
              last_modified: 4000,
              website: "https://www.other.org/signin",
              selector: "#signinpassword",
            },
            {
              id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
              last_modified: 3500,
              website: "https://some-website.com/login",
              selector: "input#user[password]",
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=%224000%22":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "5000"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          metadata: {
            signature: {},
          },
          timestamp: 5000,
          changes: [
            {
              id: "aabad965-e556-ffe7-4191-074f5dee3df3",
              deleted: true,
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=%229999%22">:
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
        ],
        status: { status: 503, statusText: "Service Unavailable" },
        responseBody: {
          code: 503,
          errno: 999,
          error: "Service Unavailable",
        },
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=%2210000%22">:
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "10001"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: ",
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=%2211000%22">:
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
        ],
        status: { status: 503, statusText: "Service Unavailable" },
        responseBody: {
          changes: [
            {
              id: "c4f021e3-f68c-4269-ad2a-d4ba87762b35",
              last_modified: 4000,
              website: "https://www.eff.org",
              selector: "#pwd",
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/password-fields?_expected=11001": {
      sampleHeaders: [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
      ],
      status: { status: 503, statusText: "Service Unavailable" },
      responseBody: {
        code: 503,
        errno: 999,
        error: "Service Unavailable",
      },
    },
    "GET:/v1/buckets/monitor/collections/changes/changeset?collection=password-fields&bucket=main&_expected=0":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          `Date: ${new Date().toUTCString()}`,
          'Etag: "1338"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          timestamp: 1338,
          changes: [
            {
              id: "fe5758d0-c67a-42d0-bb4f-8f2d75106b65",
              bucket: "main",
              collection: "password-fields",
              last_modified: 1337,
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "3000"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          metadata: {
            signature: {
              signature: "some-sig",
              x5u: `http://localhost:${port}/fake-x5u`,
            },
          },
          timestamp: 3000,
          changes: [
            {
              id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69",
              last_modified: 3000,
              website: "https://some-website.com",
              selector: "#webpage[field-pwd]",
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=%223000%22":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "3001"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          metadata: {
            signature: {
              signature: "some-sig",
              x5u: `http://localhost:${port}/fake-x5u`,
            },
          },
          timestamp: 3001,
          changes: [
            {
              id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69",
              last_modified: 3001,
              website: "https://some-website-2.com",
              selector: "#webpage[field-pwd]",
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/language-dictionaries/changeset": {
      sampleHeaders: [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
        'Etag: "5000000000000"',
      ],
      status: { status: 200, statusText: "OK" },
      responseBody: {
        timestamp: 5000000000000,
        metadata: {
          id: "language-dictionaries",
          last_modified: 1234,
          signature: {
            signature: "xyz",
            x5u: `http://localhost:${port}/fake-x5u`,
          },
        },
        changes: [
          {
            id: "xx",
            last_modified: 5000000000000,
            dictionaries: ["xx-XX@dictionaries.addons.mozilla.org"],
          },
          {
            id: "fr",
            last_modified: 5000000000000 - 1,
            deleted: true,
          },
          {
            id: "pt-BR",
            last_modified: 5000000000000 - 2,
            dictionaries: ["pt-BR@for-tests"],
          },
        ],
      },
    },
    "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=2000":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "2000"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          timestamp: 2000,
          metadata: {
            id: "with-local-fields",
            last_modified: 1234,
            signature: {
              signature: "xyz",
              x5u: `http://localhost:${port}/fake-x5u`,
            },
          },
          changes: [
            {
              id: "c74279ce-fb0a-42a6-ae11-386b567a6119",
              last_modified: 2000,
            },
          ],
        },
      },
    "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=%222000%22"n>:
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          'Etag: "3000"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          timestamp: 3000,
          metadata: {
            signature: {},
          },
          changes: [
            {
              id: "1f5c98b9-6d93-4c13-aa26-978b38695096",
              last_modified: 3000,
            },
          ],
        },
      },
    "GET:/v1/buckets/monitor/collections/changes/changeset?collection=no-mocked-responses&bucket=main&_expected=0":
      {
        sampleHeaders: [
          "Access-Control-Allow-Origin: *",
          "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
          "Content-Type: application/json; charset=UTF-8",
          "Server: waitress",
          `Date: ${new Date().toUTCString()}`,
          'Etag: "713705"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: {
          data: [
            {
              id: "07a98d1b-7c62-4344-ab18-76856b3facd8",
              bucket: "main",
              collection: "no-mocked-responses",
              last_modified: 713705,
            },
          ],
        },
      },
  };
  return (
    responses[`${req.method}:${req.path}?${req.queryString}`] ||
    responses[`${req.method}:${req.path}`] ||
    responses[req.method]
  );
}

add_task(clear_state);

add_task(async function test_hasAttachments_works_as_expected() {
  let res = await client.db.hasAttachments();
  Assert.equal(res, false"Should return false, no attachments at start");

  await client.db.saveAttachment("foo", {
    record: { id: "foo" },
    blob: new Blob(["foo"]),
  });

  res = await client.db.hasAttachments();
  Assert.equal(res, true"Should return true, just saved an attachment");

  await client.db.pruneAttachments([]);

  res = await client.db.hasAttachments();
  Assert.equal(res, false"Should return false after attachments are pruned");
});

Messung V0.5
C=95 H=99 G=96

¤ Dauer der Verarbeitung: 0.23 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge