Skip to content

Commit

Permalink
Merge pull request #50 from mozilla-services/more-integration-tests
Browse files Browse the repository at this point in the history
Added full integration test suite.
  • Loading branch information
n1k0 committed Jul 6, 2015
2 parents bead196 + d0b9995 commit c0834f5
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 70 deletions.
6 changes: 5 additions & 1 deletion src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ export default class Api {
} else if (response.status === 412) {
results.conflicts.push({
type: "outgoing",
data: response.body
local: records[index],
// TODO: Once we get record information in this response object,
// add it; for now, that's the error json body only.
// Ref https://github.com/mozilla-services/kinto/issues/122
remote: response.body
});
} else {
results.errors.push({
Expand Down
12 changes: 9 additions & 3 deletions src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,21 @@ export default class Collection {
* Adds a record to the local database.
*
* Options:
* - {Boolean} synced: Sets record status to "synced" (default: false)
* - {Boolean} synced: Sets record status to "synced" (default: false);
* - {Boolean} forceUUID: Enforces record creation using any provided UUID.
*
* @param {Object} record
* @param {Object} options
* @return {Promise}
*/
create(record, options={synced: false}) {
create(record, options={forceUUID: false, synced: false}) {
return this.open().then(() => {
if (typeof(record) !== "object")
return Promise.reject(new Error('Record is not an object.'));
return new Promise((resolve, reject) => {
const {transaction, store} = this.prepare("readwrite");
const newRecord = Object.assign({}, record, {
id: options.synced ? record.id : uuid4(),
id: options.synced || options.forceUUID ? record.id : uuid4(),
_status: options.synced ? "synced" : "created"
});
store.add(newRecord);
Expand Down Expand Up @@ -532,6 +533,11 @@ export default class Collection {
})
// Update published local records
.then(([deleted, synced]) => {
// Merge outgoing errors into sync result object
syncResultObject.add("errors", synced.errors);
// Merge outgoing conflicts into sync result object
syncResultObject.add("conflicts", synced.conflicts);
// Process local updates following published changes
return Promise.all(synced.published.map(record => {
if (record.deleted) {
// Remote deletion was successful, refect it locally
Expand Down
3 changes: 2 additions & 1 deletion test/api_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ describe("Api", () => {
.should.eventually.become({
conflicts: [{
type: "outgoing",
data: { invalid: true },
local: published[0],
remote: { invalid: true }
}],
skipped: [],
errors: [],
Expand Down
14 changes: 11 additions & 3 deletions test/collection_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,17 @@ describe("Collection", () => {
});

it("should actually persist the record into the collection", () => {
return articles.create(article).then(result => {
return articles.get(result.data.id).then(res => res.data.title);
}).should.become(article.title);
return articles.create(article)
.then(result => articles.get(result.data.id))
.then(res => res.data.title)
.should.become(article.title);
});

it("should support the forceUUID option", function() {
return articles.create({id: 42, title: "foo"}, {forceUUID: true})
.then(result => articles.get(result.data.id))
.then(res => res.data.id)
.should.become(42);
});

it("should prefix error encountered", () => {
Expand Down
254 changes: 192 additions & 62 deletions test/integration_test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"use strict";

import Collection from "../src/collection";
import { v4 as uuid4 } from "uuid";
import btoa from "btoa";
import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import Cliquetis from "../src";
import { cleanRecord } from "../src/api";

chai.use(chaiAsPromised);
chai.should();
Expand All @@ -29,77 +31,205 @@ describe("Integration tests", () => {
});
});

function testSync(data) {
return Promise.all([].concat(
// Create local unsynced records
data.localUnsynced.map(record => tasks.create(record, {forceUUID: true})),
// Create local synced records
data.localSynced.map(record => tasks.create(record, {synced: true})),
// Create remote records
tasks.api.batch("default", "tasks", data.server)
)).then(_ => {
return tasks.sync();
});
}

describe("Synchronization", () => {
const fixtures = [
{title: "task1", done: true},
{title: "task2", done: false},
{title: "task3", done: false},
];

beforeEach(() => {
return Promise.all(fixtures.map(fixture => {
return tasks.create(fixture);
}));
describe("No conflict", () => {
const testData = {
localSynced: [
{id: uuid4(), title: "task2", done: false},
{id: uuid4(), title: "task3", done: true},
],
localUnsynced: [
{id: uuid4(), title: "task4", done: false},
],
server: [
{id: uuid4(), title: "task1", done: true},
]
};
var syncResult;

beforeEach(() => {
return testSync(testData).then(res => syncResult = res);
});

it("should have an ok status", () => {
expect(syncResult.ok).eql(true);
});

it("should contain no errors", () => {
expect(syncResult.errors).to.have.length.of(0);
});

it("should have a valid lastModified value", () => {
expect(syncResult.lastModified).to.be.a("number");
});

it("should not contain conflicts", () => {
expect(syncResult.conflicts).to.have.length.of(0);
});

it("should not have skipped records", () => {
expect(syncResult.skipped).to.have.length.of(0);
});

it("should have imported server data", () => {
expect(syncResult.created).to.have.length.of(1);
expect(cleanRecord(syncResult.created[0])).eql(testData.server[0]);
});

it("should have published local unsynced records", () => {
expect(syncResult.published).to.have.length.of(1);
expect(cleanRecord(syncResult.published[0])).eql(testData.localUnsynced[0]);
});

it("should mark local records as synced", () => {
expect(syncResult.updated).to.have.length.of(2);
expect(syncResult.updated.map(r => cleanRecord(r))).to
.include(testData.server[0])
.include(testData.localUnsynced[0]);
});
});

describe("local updates", function() {
it("should update local records from server response", () => {
return tasks.sync()
.then(res => res.updated.map(r => r.title))
.should.eventually
.include("task1")
.include("task2")
.include("task3");
});

it("should publish local records to the server", () => {
return tasks.sync()
.then(res => res.published.map(r => r.title))
.should.eventually
.include("task1")
.include("task2")
.include("task3");
});

describe("Importing new remote records", function() {
var syncResult, createdId;

beforeEach(() => {
createdId = uuid4();
return tasks.api.batch("default", "tasks", [
{id: createdId, title: "task4", done: true}
])
.then(_ => tasks.sync())
.then(res => syncResult = res);
});
describe("Incoming conflict", () => {
const conflictingId = uuid4();
const testData = {
localSynced: [
{id: uuid4(), title: "task2", done: false},
{id: uuid4(), title: "task3", done: true},
],
localUnsynced: [
{id: conflictingId, title: "task4-local", done: false},
],
server: [
{id: conflictingId, title: "task4-remote", done: true},
]
};
var syncResult;

it("should list created records", function() {
expect(syncResult.created).to.have.length.of(1);
expect(syncResult.created[0].id).eql(createdId);
expect(syncResult.created[0].title).eql("task4");
});
beforeEach(() => {
return testSync(testData).then(res => syncResult = res);
});

it("should not have an ok status", () => {
expect(syncResult.ok).eql(false);
});

it("should contain no errors", () => {
expect(syncResult.errors).to.have.length.of(0);
});

it("should have a valid lastModified value", () => {
expect(syncResult.lastModified).to.be.a("number");
});

it("should import records", function() {
return tasks.list()
.then(res => res.data.map(r => r.title))
.should.eventually
.include("task1")
.include("task2")
.include("task3")
.include("task4");
it("should have the incoming conflict listed", () => {
expect(syncResult.conflicts).to.have.length.of(1);
expect(syncResult.conflicts[0].type).eql("incoming");
expect(cleanRecord(syncResult.conflicts[0].local)).eql({
id: conflictingId,
title: "task4-local",
done: false,
});
expect(cleanRecord(syncResult.conflicts[0].remote)).eql({
id: conflictingId,
title: "task4-remote",
done: true,
});
});

it("should not have skipped records", () => {
expect(syncResult.skipped).to.have.length.of(0);
});

it("should not have imported anything", () => {
expect(syncResult.created).to.have.length.of(0);
});

it("should not have published anything", () => {
expect(syncResult.published).to.have.length.of(0);
});

it("should not have updated anything", () => {
expect(syncResult.updated).to.have.length.of(0);
});
});

describe("remote updates", function() {
it("should have updated the server", function() {
return tasks.sync()
.then(_ => tasks.api.fetchChangesSince("default", "tasks"))
.then(res => res.changes.map(r => r.title))
.should.eventually
.include("task1")
.include("task2")
.include("task3");
describe("Outgoing conflict", () => {
var syncResult;

beforeEach(() => {
return fetch(`${TEST_KINTO_SERVER}/buckets/default/collections/tasks/records`, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Basic " + btoa("user:pass"),
},
body: JSON.stringify({data: {title: "foo"}})
})
.then(_ => tasks.sync())
.then(res => {
return tasks.update(Object.assign({}, res.created[0], {
last_modified: undefined
}));
})
.then(res => tasks.sync())
.then(res => {
syncResult = res;
});
});

it("should not have an ok status", () => {
expect(syncResult.ok).eql(false);
});

it("should contain no errors", () => {
expect(syncResult.errors).to.have.length.of(0);
});

it("should have a valid lastModified value", () => {
expect(syncResult.lastModified).to.be.a("number");
});

it("should have the outgoing conflict listed", () => {
expect(syncResult.conflicts).to.have.length.of(1);
expect(syncResult.conflicts[0].type).eql("outgoing");
expect(syncResult.conflicts[0].local.title).eql("foo");
// TODO: https://github.com/mozilla-services/kinto/issues/122
expect(cleanRecord(syncResult.conflicts[0].remote)).eql({
"code": 412,
"errno": 999,
"error": "Precondition Failed",
"message": "Failed batch subrequest",
});
});

it("should not have skipped records", () => {
expect(syncResult.skipped).to.have.length.of(0);
});

it("should not have imported anything", () => {
expect(syncResult.created).to.have.length.of(0);
});

it("should not have published anything", () => {
expect(syncResult.published).to.have.length.of(0);
});

it("should not have updated anything", () => {
expect(syncResult.updated).to.have.length.of(0);
});
});
});
Expand Down

0 comments on commit c0834f5

Please sign in to comment.