test_kinto.js (16745B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { Kinto } = ChromeUtils.importESModule( 5 "resource://services-common/kinto-offline-client.sys.mjs" 6 ); 7 const { FirefoxAdapter } = ChromeUtils.importESModule( 8 "resource://services-common/kinto-storage-adapter.sys.mjs" 9 ); 10 11 var server; 12 13 // set up what we need to make storage adapters 14 const kintoFilename = "kinto.sqlite"; 15 16 function do_get_kinto_sqliteHandle() { 17 return FirefoxAdapter.openConnection({ path: kintoFilename }); 18 } 19 20 function do_get_kinto_collection(sqliteHandle, collection = "test_collection") { 21 let config = { 22 remote: `http://localhost:${server.identity.primaryPort}/v1/`, 23 headers: { Authorization: "Basic " + btoa("user:pass") }, 24 adapter: FirefoxAdapter, 25 adapterOptions: { sqliteHandle }, 26 }; 27 return new Kinto(config).collection(collection); 28 } 29 30 async function clear_collection() { 31 let sqliteHandle; 32 try { 33 sqliteHandle = await do_get_kinto_sqliteHandle(); 34 const collection = do_get_kinto_collection(sqliteHandle); 35 await collection.clear(); 36 } finally { 37 await sqliteHandle.close(); 38 } 39 } 40 41 // test some operations on a local collection 42 add_task(async function test_kinto_add_get() { 43 let sqliteHandle; 44 try { 45 sqliteHandle = await do_get_kinto_sqliteHandle(); 46 const collection = do_get_kinto_collection(sqliteHandle); 47 48 let newRecord = { foo: "bar" }; 49 // check a record is created 50 let createResult = await collection.create(newRecord); 51 Assert.equal(createResult.data.foo, newRecord.foo); 52 // check getting the record gets the same info 53 let getResult = await collection.get(createResult.data.id); 54 deepEqual(createResult.data, getResult.data); 55 // check what happens if we create the same item again (it should throw 56 // since you can't create with id) 57 try { 58 await collection.create(createResult.data); 59 do_throw("Creation of a record with an id should fail"); 60 } catch (err) {} 61 // try a few creates without waiting for the first few to resolve 62 let promises = []; 63 promises.push(collection.create(newRecord)); 64 promises.push(collection.create(newRecord)); 65 promises.push(collection.create(newRecord)); 66 await collection.create(newRecord); 67 await Promise.all(promises); 68 } finally { 69 await sqliteHandle.close(); 70 } 71 }); 72 73 add_task(clear_collection); 74 75 // test some operations on multiple connections 76 add_task(async function test_kinto_add_get() { 77 let sqliteHandle; 78 try { 79 sqliteHandle = await do_get_kinto_sqliteHandle(); 80 const collection1 = do_get_kinto_collection(sqliteHandle); 81 const collection2 = do_get_kinto_collection( 82 sqliteHandle, 83 "test_collection_2" 84 ); 85 86 let newRecord = { foo: "bar" }; 87 88 // perform several write operations alternately without waiting for promises 89 // to resolve 90 let promises = []; 91 for (let i = 0; i < 10; i++) { 92 promises.push(collection1.create(newRecord)); 93 promises.push(collection2.create(newRecord)); 94 } 95 96 // ensure subsequent operations still work 97 await Promise.all([ 98 collection1.create(newRecord), 99 collection2.create(newRecord), 100 ]); 101 await Promise.all(promises); 102 } finally { 103 await sqliteHandle.close(); 104 } 105 }); 106 107 add_task(clear_collection); 108 109 add_task(async function test_kinto_update() { 110 let sqliteHandle; 111 try { 112 sqliteHandle = await do_get_kinto_sqliteHandle(); 113 const collection = do_get_kinto_collection(sqliteHandle); 114 const newRecord = { foo: "bar" }; 115 // check a record is created 116 let createResult = await collection.create(newRecord); 117 Assert.equal(createResult.data.foo, newRecord.foo); 118 Assert.equal(createResult.data._status, "created"); 119 // check we can update this OK 120 let copiedRecord = Object.assign(createResult.data, {}); 121 deepEqual(createResult.data, copiedRecord); 122 copiedRecord.foo = "wibble"; 123 let updateResult = await collection.update(copiedRecord); 124 // check the field was updated 125 Assert.equal(updateResult.data.foo, copiedRecord.foo); 126 // check the status is still "created", since we haven't synced 127 // the record 128 Assert.equal(updateResult.data._status, "created"); 129 } finally { 130 await sqliteHandle.close(); 131 } 132 }); 133 134 add_task(clear_collection); 135 136 add_task(async function test_kinto_clear() { 137 let sqliteHandle; 138 try { 139 sqliteHandle = await do_get_kinto_sqliteHandle(); 140 const collection = do_get_kinto_collection(sqliteHandle); 141 142 // create an expected number of records 143 const expected = 10; 144 const newRecord = { foo: "bar" }; 145 for (let i = 0; i < expected; i++) { 146 await collection.create(newRecord); 147 } 148 // check the collection contains the correct number 149 let list = await collection.list(); 150 Assert.equal(list.data.length, expected); 151 // clear the collection and check again - should be 0 152 await collection.clear(); 153 list = await collection.list(); 154 Assert.equal(list.data.length, 0); 155 } finally { 156 await sqliteHandle.close(); 157 } 158 }); 159 160 add_task(clear_collection); 161 162 add_task(async function test_kinto_delete() { 163 let sqliteHandle; 164 try { 165 sqliteHandle = await do_get_kinto_sqliteHandle(); 166 const collection = do_get_kinto_collection(sqliteHandle); 167 const newRecord = { foo: "bar" }; 168 // check a record is created 169 let createResult = await collection.create(newRecord); 170 Assert.equal(createResult.data.foo, newRecord.foo); 171 // check getting the record gets the same info 172 let getResult = await collection.get(createResult.data.id); 173 deepEqual(createResult.data, getResult.data); 174 // delete that record 175 let deleteResult = await collection.delete(createResult.data.id); 176 // check the ID is set on the result 177 Assert.equal(getResult.data.id, deleteResult.data.id); 178 // and check that get no longer returns the record 179 try { 180 getResult = await collection.get(createResult.data.id); 181 do_throw("there should not be a result"); 182 } catch (e) {} 183 } finally { 184 await sqliteHandle.close(); 185 } 186 }); 187 188 add_task(async function test_kinto_list() { 189 let sqliteHandle; 190 try { 191 sqliteHandle = await do_get_kinto_sqliteHandle(); 192 const collection = do_get_kinto_collection(sqliteHandle); 193 const expected = 10; 194 const created = []; 195 for (let i = 0; i < expected; i++) { 196 let newRecord = { foo: "test " + i }; 197 let createResult = await collection.create(newRecord); 198 created.push(createResult.data); 199 } 200 // check the collection contains the correct number 201 let list = await collection.list(); 202 Assert.equal(list.data.length, expected); 203 204 // check that all created records exist in the retrieved list 205 for (let createdRecord of created) { 206 let found = false; 207 for (let retrievedRecord of list.data) { 208 if (createdRecord.id == retrievedRecord.id) { 209 deepEqual(createdRecord, retrievedRecord); 210 found = true; 211 } 212 } 213 Assert.ok(found); 214 } 215 } finally { 216 await sqliteHandle.close(); 217 } 218 }); 219 220 add_task(clear_collection); 221 222 add_task(async function test_importBulk_ignores_already_imported_records() { 223 let sqliteHandle; 224 try { 225 sqliteHandle = await do_get_kinto_sqliteHandle(); 226 const collection = do_get_kinto_collection(sqliteHandle); 227 const record = { 228 id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", 229 title: "foo", 230 last_modified: 1457896541, 231 }; 232 await collection.importBulk([record]); 233 let impactedRecords = await collection.importBulk([record]); 234 Assert.equal(impactedRecords.length, 0); 235 } finally { 236 await sqliteHandle.close(); 237 } 238 }); 239 240 add_task(clear_collection); 241 242 add_task(async function test_loadDump_should_overwrite_old_records() { 243 let sqliteHandle; 244 try { 245 sqliteHandle = await do_get_kinto_sqliteHandle(); 246 const collection = do_get_kinto_collection(sqliteHandle); 247 const record = { 248 id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", 249 title: "foo", 250 last_modified: 1457896541, 251 }; 252 await collection.loadDump([record]); 253 const updated = Object.assign({}, record, { last_modified: 1457896543 }); 254 let impactedRecords = await collection.loadDump([updated]); 255 Assert.equal(impactedRecords.length, 1); 256 } finally { 257 await sqliteHandle.close(); 258 } 259 }); 260 261 add_task(clear_collection); 262 263 add_task(async function test_loadDump_should_not_overwrite_unsynced_records() { 264 let sqliteHandle; 265 try { 266 sqliteHandle = await do_get_kinto_sqliteHandle(); 267 const collection = do_get_kinto_collection(sqliteHandle); 268 const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f"; 269 await collection.create( 270 { id: recordId, title: "foo" }, 271 { useRecordId: true } 272 ); 273 const record = { id: recordId, title: "bar", last_modified: 1457896541 }; 274 let impactedRecords = await collection.loadDump([record]); 275 Assert.equal(impactedRecords.length, 0); 276 } finally { 277 await sqliteHandle.close(); 278 } 279 }); 280 281 add_task(clear_collection); 282 283 add_task( 284 async function test_loadDump_should_not_overwrite_records_without_last_modified() { 285 let sqliteHandle; 286 try { 287 sqliteHandle = await do_get_kinto_sqliteHandle(); 288 const collection = do_get_kinto_collection(sqliteHandle); 289 const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f"; 290 await collection.create({ id: recordId, title: "foo" }, { synced: true }); 291 const record = { id: recordId, title: "bar", last_modified: 1457896541 }; 292 let impactedRecords = await collection.loadDump([record]); 293 Assert.equal(impactedRecords.length, 0); 294 } finally { 295 await sqliteHandle.close(); 296 } 297 } 298 ); 299 300 add_task(clear_collection); 301 302 // Now do some sanity checks against a server - we're not looking to test 303 // core kinto.js functionality here (there is excellent test coverage in 304 // kinto.js), more making sure things are basically working as expected. 305 add_task(async function test_kinto_sync() { 306 const configPath = "/v1/"; 307 const metadataPath = "/v1/buckets/default/collections/test_collection"; 308 const recordsPath = "/v1/buckets/default/collections/test_collection/records"; 309 // register a handler 310 function handleResponse(request, response) { 311 try { 312 const sampled = getSampleResponse(request, server.identity.primaryPort); 313 if (!sampled) { 314 do_throw( 315 `unexpected ${request.method} request for ${request.path}?${request.queryString}` 316 ); 317 } 318 319 response.setStatusLine( 320 null, 321 sampled.status.status, 322 sampled.status.statusText 323 ); 324 // send the headers 325 for (let headerLine of sampled.sampleHeaders) { 326 let headerElements = headerLine.split(":"); 327 response.setHeader(headerElements[0], headerElements[1].trimLeft()); 328 } 329 response.setHeader("Date", new Date().toUTCString()); 330 331 response.write(sampled.responseBody); 332 } catch (e) { 333 dump(`${e}\n`); 334 } 335 } 336 server.registerPathHandler(configPath, handleResponse); 337 server.registerPathHandler(metadataPath, handleResponse); 338 server.registerPathHandler(recordsPath, handleResponse); 339 340 // create an empty collection, sync to populate 341 let sqliteHandle; 342 try { 343 let result; 344 sqliteHandle = await do_get_kinto_sqliteHandle(); 345 const collection = do_get_kinto_collection(sqliteHandle); 346 347 result = await collection.sync(); 348 Assert.ok(result.ok); 349 350 // our test data has a single record; it should be in the local collection 351 let list = await collection.list(); 352 Assert.equal(list.data.length, 1); 353 354 // now sync again; we should now have 2 records 355 result = await collection.sync(); 356 Assert.ok(result.ok); 357 list = await collection.list(); 358 Assert.equal(list.data.length, 2); 359 360 // sync again; the second records should have been modified 361 const before = list.data[0].title; 362 result = await collection.sync(); 363 Assert.ok(result.ok); 364 list = await collection.list(); 365 const after = list.data[1].title; 366 Assert.notEqual(before, after); 367 368 const manualID = list.data[0].id; 369 Assert.equal(list.data.length, 3); 370 Assert.equal(manualID, "some-manually-chosen-id"); 371 } finally { 372 await sqliteHandle.close(); 373 } 374 }); 375 376 function run_test() { 377 // Set up an HTTP Server 378 server = new HttpServer(); 379 server.start(-1); 380 381 run_next_test(); 382 383 registerCleanupFunction(function () { 384 server.stop(function () {}); 385 }); 386 } 387 388 // get a response for a given request from sample data 389 function getSampleResponse(req, port) { 390 const responses = { 391 OPTIONS: { 392 sampleHeaders: [ 393 "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", 394 "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS", 395 "Access-Control-Allow-Origin: *", 396 "Content-Type: application/json; charset=UTF-8", 397 "Server: waitress", 398 ], 399 status: { status: 200, statusText: "OK" }, 400 responseBody: "null", 401 }, 402 "GET:/v1/?": { 403 sampleHeaders: [ 404 "Access-Control-Allow-Origin: *", 405 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 406 "Content-Type: application/json; charset=UTF-8", 407 "Server: waitress", 408 ], 409 status: { status: 200, statusText: "OK" }, 410 responseBody: JSON.stringify({ 411 settings: { 412 batch_max_requests: 25, 413 }, 414 url: `http://localhost:${port}/v1/`, 415 documentation: "https://kinto.readthedocs.org/", 416 version: "1.5.1", 417 commit: "cbc6f58", 418 hello: "kinto", 419 }), 420 }, 421 "GET:/v1/buckets/default/collections/test_collection": { 422 sampleHeaders: [ 423 "Access-Control-Allow-Origin: *", 424 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 425 "Content-Type: application/json; charset=UTF-8", 426 "Server: waitress", 427 'Etag: "1234"', 428 ], 429 status: { status: 200, statusText: "OK" }, 430 responseBody: JSON.stringify({ 431 data: { 432 id: "test_collection", 433 last_modified: 1234, 434 }, 435 }), 436 }, 437 "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": 438 { 439 sampleHeaders: [ 440 "Access-Control-Allow-Origin: *", 441 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 442 "Content-Type: application/json; charset=UTF-8", 443 "Server: waitress", 444 'Etag: "1445606341071"', 445 ], 446 status: { status: 200, statusText: "OK" }, 447 responseBody: JSON.stringify({ 448 data: [ 449 { 450 last_modified: 1445606341071, 451 done: false, 452 id: "68db8313-686e-4fff-835e-07d78ad6f2af", 453 title: "New test", 454 }, 455 ], 456 }), 457 }, 458 "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": 459 { 460 sampleHeaders: [ 461 "Access-Control-Allow-Origin: *", 462 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 463 "Content-Type: application/json; charset=UTF-8", 464 "Server: waitress", 465 'Etag: "1445607941223"', 466 ], 467 status: { status: 200, statusText: "OK" }, 468 responseBody: JSON.stringify({ 469 data: [ 470 { 471 last_modified: 1445607941223, 472 done: false, 473 id: "901967b0-f729-4b30-8d8d-499cba7f4b1d", 474 title: "Another new test", 475 }, 476 ], 477 }), 478 }, 479 "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": 480 { 481 sampleHeaders: [ 482 "Access-Control-Allow-Origin: *", 483 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 484 "Content-Type: application/json; charset=UTF-8", 485 "Server: waitress", 486 'Etag: "1445607541267"', 487 ], 488 status: { status: 200, statusText: "OK" }, 489 responseBody: JSON.stringify({ 490 data: [ 491 { 492 last_modified: 1445607541265, 493 done: false, 494 id: "901967b0-f729-4b30-8d8d-499cba7f4b1d", 495 title: "Modified title", 496 }, 497 { 498 last_modified: 1445607541267, 499 done: true, 500 id: "some-manually-chosen-id", 501 title: "New record with custom ID", 502 }, 503 ], 504 }), 505 }, 506 }; 507 return ( 508 responses[`${req.method}:${req.path}?${req.queryString}`] || 509 responses[`${req.method}:${req.path}`] || 510 responses[req.method] 511 ); 512 }