diff options
Diffstat (limited to 'services/common/tests/unit/test_kinto.js')
-rw-r--r-- | services/common/tests/unit/test_kinto.js | 515 |
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] + ); +} |