summaryrefslogtreecommitdiffstats
path: root/services/common/tests/unit/test_kinto.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/common/tests/unit/test_kinto.js')
-rw-r--r--services/common/tests/unit/test_kinto.js515
1 files changed, 515 insertions, 0 deletions
diff --git a/services/common/tests/unit/test_kinto.js b/services/common/tests/unit/test_kinto.js
new file mode 100644
index 0000000000..8bd9add5bc
--- /dev/null
+++ b/services/common/tests/unit/test_kinto.js
@@ -0,0 +1,515 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { Kinto } = ChromeUtils.import(
+ "resource://services-common/kinto-offline-client.js"
+);
+const { FirefoxAdapter } = ChromeUtils.import(
+ "resource://services-common/kinto-storage-adapter.js"
+);
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+var server;
+
+// set up what we need to make storage adapters
+const kintoFilename = "kinto.sqlite";
+
+function do_get_kinto_sqliteHandle() {
+ return FirefoxAdapter.openConnection({ path: kintoFilename });
+}
+
+function do_get_kinto_collection(sqliteHandle, collection = "test_collection") {
+ let config = {
+ remote: `http://localhost:${server.identity.primaryPort}/v1/`,
+ headers: { Authorization: "Basic " + btoa("user:pass") },
+ adapter: FirefoxAdapter,
+ adapterOptions: { sqliteHandle },
+ };
+ return new Kinto(config).collection(collection);
+}
+
+async function clear_collection() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ await collection.clear();
+ } finally {
+ await sqliteHandle.close();
+ }
+}
+
+// test some operations on a local collection
+add_task(async function test_kinto_add_get() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+
+ let newRecord = { foo: "bar" };
+ // check a record is created
+ let createResult = await collection.create(newRecord);
+ Assert.equal(createResult.data.foo, newRecord.foo);
+ // check getting the record gets the same info
+ let getResult = await collection.get(createResult.data.id);
+ deepEqual(createResult.data, getResult.data);
+ // check what happens if we create the same item again (it should throw
+ // since you can't create with id)
+ try {
+ await collection.create(createResult.data);
+ do_throw("Creation of a record with an id should fail");
+ } catch (err) {}
+ // try a few creates without waiting for the first few to resolve
+ let promises = [];
+ promises.push(collection.create(newRecord));
+ promises.push(collection.create(newRecord));
+ promises.push(collection.create(newRecord));
+ await collection.create(newRecord);
+ await Promise.all(promises);
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+// test some operations on multiple connections
+add_task(async function test_kinto_add_get() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection1 = do_get_kinto_collection(sqliteHandle);
+ const collection2 = do_get_kinto_collection(
+ sqliteHandle,
+ "test_collection_2"
+ );
+
+ let newRecord = { foo: "bar" };
+
+ // perform several write operations alternately without waiting for promises
+ // to resolve
+ let promises = [];
+ for (let i = 0; i < 10; i++) {
+ promises.push(collection1.create(newRecord));
+ promises.push(collection2.create(newRecord));
+ }
+
+ // ensure subsequent operations still work
+ await Promise.all([
+ collection1.create(newRecord),
+ collection2.create(newRecord),
+ ]);
+ await Promise.all(promises);
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(async function test_kinto_update() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const newRecord = { foo: "bar" };
+ // check a record is created
+ let createResult = await collection.create(newRecord);
+ Assert.equal(createResult.data.foo, newRecord.foo);
+ Assert.equal(createResult.data._status, "created");
+ // check we can update this OK
+ let copiedRecord = Object.assign(createResult.data, {});
+ deepEqual(createResult.data, copiedRecord);
+ copiedRecord.foo = "wibble";
+ let updateResult = await collection.update(copiedRecord);
+ // check the field was updated
+ Assert.equal(updateResult.data.foo, copiedRecord.foo);
+ // check the status is still "created", since we haven't synced
+ // the record
+ Assert.equal(updateResult.data._status, "created");
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(async function test_kinto_clear() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+
+ // create an expected number of records
+ const expected = 10;
+ const newRecord = { foo: "bar" };
+ for (let i = 0; i < expected; i++) {
+ await collection.create(newRecord);
+ }
+ // check the collection contains the correct number
+ let list = await collection.list();
+ Assert.equal(list.data.length, expected);
+ // clear the collection and check again - should be 0
+ await collection.clear();
+ list = await collection.list();
+ Assert.equal(list.data.length, 0);
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(async function test_kinto_delete() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const newRecord = { foo: "bar" };
+ // check a record is created
+ let createResult = await collection.create(newRecord);
+ Assert.equal(createResult.data.foo, newRecord.foo);
+ // check getting the record gets the same info
+ let getResult = await collection.get(createResult.data.id);
+ deepEqual(createResult.data, getResult.data);
+ // delete that record
+ let deleteResult = await collection.delete(createResult.data.id);
+ // check the ID is set on the result
+ Assert.equal(getResult.data.id, deleteResult.data.id);
+ // and check that get no longer returns the record
+ try {
+ getResult = await collection.get(createResult.data.id);
+ do_throw("there should not be a result");
+ } catch (e) {}
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(async function test_kinto_list() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const expected = 10;
+ const created = [];
+ for (let i = 0; i < expected; i++) {
+ let newRecord = { foo: "test " + i };
+ let createResult = await collection.create(newRecord);
+ created.push(createResult.data);
+ }
+ // check the collection contains the correct number
+ let list = await collection.list();
+ Assert.equal(list.data.length, expected);
+
+ // check that all created records exist in the retrieved list
+ for (let createdRecord of created) {
+ let found = false;
+ for (let retrievedRecord of list.data) {
+ if (createdRecord.id == retrievedRecord.id) {
+ deepEqual(createdRecord, retrievedRecord);
+ found = true;
+ }
+ }
+ Assert.ok(found);
+ }
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(async function test_importBulk_ignores_already_imported_records() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const record = {
+ id: "41b71c13-17e9-4ee3-9268-6a41abf9730f",
+ title: "foo",
+ last_modified: 1457896541,
+ };
+ await collection.importBulk([record]);
+ let impactedRecords = await collection.importBulk([record]);
+ Assert.equal(impactedRecords.length, 0);
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(async function test_loadDump_should_overwrite_old_records() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const record = {
+ id: "41b71c13-17e9-4ee3-9268-6a41abf9730f",
+ title: "foo",
+ last_modified: 1457896541,
+ };
+ await collection.loadDump([record]);
+ const updated = Object.assign({}, record, { last_modified: 1457896543 });
+ let impactedRecords = await collection.loadDump([updated]);
+ Assert.equal(impactedRecords.length, 1);
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(async function test_loadDump_should_not_overwrite_unsynced_records() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
+ await collection.create(
+ { id: recordId, title: "foo" },
+ { useRecordId: true }
+ );
+ const record = { id: recordId, title: "bar", last_modified: 1457896541 };
+ let impactedRecords = await collection.loadDump([record]);
+ Assert.equal(impactedRecords.length, 0);
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+add_task(clear_collection);
+
+add_task(
+ async function test_loadDump_should_not_overwrite_records_without_last_modified() {
+ let sqliteHandle;
+ try {
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+ const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
+ await collection.create({ id: recordId, title: "foo" }, { synced: true });
+ const record = { id: recordId, title: "bar", last_modified: 1457896541 };
+ let impactedRecords = await collection.loadDump([record]);
+ Assert.equal(impactedRecords.length, 0);
+ } finally {
+ await sqliteHandle.close();
+ }
+ }
+);
+
+add_task(clear_collection);
+
+// Now do some sanity checks against a server - we're not looking to test
+// core kinto.js functionality here (there is excellent test coverage in
+// kinto.js), more making sure things are basically working as expected.
+add_task(async function test_kinto_sync() {
+ const configPath = "/v1/";
+ const metadataPath = "/v1/buckets/default/collections/test_collection";
+ const recordsPath = "/v1/buckets/default/collections/test_collection/records";
+ // register a handler
+ function handleResponse(request, response) {
+ try {
+ const sampled = getSampleResponse(request, server.identity.primaryPort);
+ if (!sampled) {
+ do_throw(
+ `unexpected ${request.method} request for ${request.path}?${request.queryString}`
+ );
+ }
+
+ response.setStatusLine(
+ null,
+ sampled.status.status,
+ sampled.status.statusText
+ );
+ // send the headers
+ for (let headerLine of sampled.sampleHeaders) {
+ let headerElements = headerLine.split(":");
+ response.setHeader(headerElements[0], headerElements[1].trimLeft());
+ }
+ response.setHeader("Date", new Date().toUTCString());
+
+ response.write(sampled.responseBody);
+ } catch (e) {
+ dump(`${e}\n`);
+ }
+ }
+ server.registerPathHandler(configPath, handleResponse);
+ server.registerPathHandler(metadataPath, handleResponse);
+ server.registerPathHandler(recordsPath, handleResponse);
+
+ // create an empty collection, sync to populate
+ let sqliteHandle;
+ try {
+ let result;
+ sqliteHandle = await do_get_kinto_sqliteHandle();
+ const collection = do_get_kinto_collection(sqliteHandle);
+
+ result = await collection.sync();
+ Assert.ok(result.ok);
+
+ // our test data has a single record; it should be in the local collection
+ let list = await collection.list();
+ Assert.equal(list.data.length, 1);
+
+ // now sync again; we should now have 2 records
+ result = await collection.sync();
+ Assert.ok(result.ok);
+ list = await collection.list();
+ Assert.equal(list.data.length, 2);
+
+ // sync again; the second records should have been modified
+ const before = list.data[0].title;
+ result = await collection.sync();
+ Assert.ok(result.ok);
+ list = await collection.list();
+ const after = list.data[1].title;
+ Assert.notEqual(before, after);
+
+ const manualID = list.data[0].id;
+ Assert.equal(list.data.length, 3);
+ Assert.equal(manualID, "some-manually-chosen-id");
+ } finally {
+ await sqliteHandle.close();
+ }
+});
+
+function run_test() {
+ // Set up an HTTP Server
+ server = new HttpServer();
+ server.start(-1);
+
+ run_next_test();
+
+ registerCleanupFunction(function() {
+ server.stop(function() {});
+ });
+}
+
+// get a response for a given request from sample data
+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: JSON.stringify({
+ 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/default/collections/test_collection": {
+ 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: "1234"',
+ ],
+ status: { status: 200, statusText: "OK" },
+ responseBody: JSON.stringify({
+ data: {
+ id: "test_collection",
+ last_modified: 1234,
+ },
+ }),
+ },
+ "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": {
+ 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: "1445606341071"',
+ ],
+ status: { status: 200, statusText: "OK" },
+ responseBody: JSON.stringify({
+ data: [
+ {
+ last_modified: 1445606341071,
+ done: false,
+ id: "68db8313-686e-4fff-835e-07d78ad6f2af",
+ title: "New test",
+ },
+ ],
+ }),
+ },
+ "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": {
+ 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: "1445607941223"',
+ ],
+ status: { status: 200, statusText: "OK" },
+ responseBody: JSON.stringify({
+ data: [
+ {
+ last_modified: 1445607941223,
+ done: false,
+ id: "901967b0-f729-4b30-8d8d-499cba7f4b1d",
+ title: "Another new test",
+ },
+ ],
+ }),
+ },
+ "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": {
+ 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: "1445607541267"',
+ ],
+ status: { status: 200, statusText: "OK" },
+ responseBody: JSON.stringify({
+ data: [
+ {
+ last_modified: 1445607541265,
+ done: false,
+ id: "901967b0-f729-4b30-8d8d-499cba7f4b1d",
+ title: "Modified title",
+ },
+ {
+ last_modified: 1445607541267,
+ done: true,
+ id: "some-manually-chosen-id",
+ title: "New record with custom ID",
+ },
+ ],
+ }),
+ },
+ };
+ return (
+ responses[`${req.method}:${req.path}?${req.queryString}`] ||
+ responses[`${req.method}:${req.path}`] ||
+ responses[req.method]
+ );
+}