diff --git a/services/common/tests/unit/test_kinto.js b/services/common/tests/unit/test_kinto.js new file mode 100644 index 00000000000..a77d3846f99 --- /dev/null +++ b/services/common/tests/unit/test_kinto.js @@ -0,0 +1,325 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://services-common/moz-kinto-client.js") +Cu.import("resource://testing-common/httpd.js"); + +const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); + +var server; + +// set up what we need to make storage adapters +const Kinto = loadKinto(); +const FirefoxAdapter = Kinto.adapters.FirefoxAdapter; +const kintoFilename = "kinto.sqlite"; + +let kintoClient; + +function do_get_kinto_collection() { + if (!kintoClient) { + let config = { + remote:`http://localhost:${server.identity.primaryPort}/v1/`, + headers: {Authorization: "Basic " + btoa("user:pass")}, + adapter: FirefoxAdapter + }; + kintoClient = new Kinto(config); + } + return kintoClient.collection("test_collection"); +} + +function* clear_collection() { + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + yield collection.clear(); + } finally { + yield collection.db.close(); + } +} + +// test some operations on a local collection +add_task(function* test_kinto_add_get() { + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + + let newRecord = { foo: "bar" }; + // check a record is created + let createResult = yield collection.create(newRecord); + do_check_eq(createResult.data.foo, newRecord.foo); + // check getting the record gets the same info + let getResult = yield 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 { + yield 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)); + yield collection.create(newRecord); + yield Promise.all(promises); + } finally { + yield collection.db.close(); + } +}); + +add_task(clear_collection); + +// test some operations on multiple connections +add_task(function* test_kinto_add_get() { + const collection1 = do_get_kinto_collection(); + const collection2 = kintoClient.collection("test_collection_2"); + + try { + yield collection1.db.open(); + yield collection2.db.open(); + + 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 + yield Promise.all([collection1.create(newRecord), + collection2.create(newRecord)]); + yield Promise.all(promises); + } finally { + yield collection1.db.close(); + yield collection2.db.close(); + } +}); + +add_task(clear_collection); + +add_task(function* test_kinto_update() { + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + const newRecord = { foo: "bar" }; + // check a record is created + let createResult = yield collection.create(newRecord); + do_check_eq(createResult.data.foo, newRecord.foo); + do_check_eq(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 = yield collection.update(copiedRecord); + // check the field was updated + do_check_eq(updateResult.data.foo, copiedRecord.foo); + // check the status has changed + do_check_eq(updateResult.data._status, "updated"); + } finally { + yield collection.db.close(); + } +}); + +add_task(clear_collection); + +add_task(function* test_kinto_clear() { + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + + // create an expected number of records + const expected = 10; + const newRecord = { foo: "bar" }; + for (let i = 0; i < expected; i++) { + yield collection.create(newRecord); + } + // check the collection contains the correct number + let list = yield collection.list(); + do_check_eq(list.data.length, expected); + // clear the collection and check again - should be 0 + yield collection.clear(); + list = yield collection.list(); + do_check_eq(list.data.length, 0); + } finally { + yield collection.db.close(); + } +}); + +add_task(clear_collection); + +add_task(function* test_kinto_delete(){ + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + const newRecord = { foo: "bar" }; + // check a record is created + let createResult = yield collection.create(newRecord); + do_check_eq(createResult.data.foo, newRecord.foo); + // check getting the record gets the same info + let getResult = yield collection.get(createResult.data.id); + deepEqual(createResult.data, getResult.data); + // delete that record + let deleteResult = yield collection.delete(createResult.data.id); + // check the ID is set on the result + do_check_eq(getResult.data.id, deleteResult.data.id); + // and check that get no longer returns the record + try { + getResult = yield collection.get(createResult.data.id); + do_throw("there should not be a result"); + } catch (e) { } + } finally { + yield collection.db.close(); + } +}); + +add_task(function* test_kinto_list(){ + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + const expected = 10; + const created = []; + for (let i = 0; i < expected; i++) { + let newRecord = { foo: "test " + i }; + let createResult = yield collection.create(newRecord); + created.push(createResult.data); + } + // check the collection contains the correct number + let list = yield collection.list(); + do_check_eq(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; + } + } + do_check_true(found); + } + } finally { + yield collection.db.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(function* test_kinto_sync(){ + const configPath = "/v1/"; + 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(recordsPath, handleResponse); + + // create an empty collection, sync to populate + const collection = do_get_kinto_collection(); + try { + yield collection.db.open(); + yield collection.sync(); + + // our test data has a single record; it should be in the local collection + let list = yield collection.list(); + do_check_eq(list.data.length, 1); + + // now sync again; we should now have 2 records + yield collection.sync(); + list = yield collection.list(); + do_check_eq(list.data.length, 2); + } finally { + yield collection.db.close(); + } +}); + +function run_test() { + // Set up an HTTP Server + server = new HttpServer(); + server.start(-1); + + run_next_test(); + + do_register_cleanup(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":{"cliquet.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/records?": { + "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?_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"}]}) + } + }; + return responses[`${req.method}:${req.path}?${req.queryString}`] || + responses[req.method]; + +} diff --git a/services/common/tests/unit/xpcshell.ini b/services/common/tests/unit/xpcshell.ini index 469aefef98e..aeee2436533 100644 --- a/services/common/tests/unit/xpcshell.ini +++ b/services/common/tests/unit/xpcshell.ini @@ -9,6 +9,7 @@ support-files = # Test load modules first so syntax failures are caught early. [test_load_modules.js] +[test_kinto.js] [test_storage_adapter.js] [test_utils_atob.js]