Mailspring/spec/tasks/syncback-model-task-spec.es6

212 lines
6.7 KiB
Text
Raw Normal View History

feat(metadata): add cloudState that sync with Metadata service Summary: Now all plugins get passed a `cloudState` object to their `activate` method. The `cloudState` object is an instance of `CloudState` and acts like a key-value store backed by the yet-to-be-implemented Metadata service. It has a `get`, `getAll`, and `observe` method. The `observe` method returns a new `Rx.Observable` for the given key. It has a `set`, and `unset` method that doesn't actually mutate state, but rather dispatches new `Task`s to Create, Update, and Delete `Metadata` objects. The whole object is backed by `Metadata` objects. Since these are standard Database Objects that will appear on the delta sync streaming API, any updates from the server will automatically propagate down to listening views via the `Rx.Observable`s. Additionally, there is a new `N1-Send-Later` stub plugin that demonstrates how to use the `cloudState`. There are few other minor refactors included in this diff: **Generic CUD Tasks**: There is now a generic `CreateModelTask`, `UpdateModelTask`, and `DestroyModelTask`. These can either be used as-is or trivially overridden to easily update simple objects. Hopefully all of the boilerplate rollback, error handling, and undo logic won't have to be re-duplicated on every task. There are also tests for these tasks. We use them to perform mutating actions on `Metadata` objects. **New `boundProps` for `InjectedComponents`**: When making the `N1-Send_later` plugin, I realized that the injected component needed to get the `cloudState` somehow. Traditionally components would require Stores and load data that way, but these are setup at `require`-time. Now that `cloudState` only is available on `activate` we needed a way to get the data to the components. There's now the concept of `boundProps` which will be props added to the Component when it gets injected. This required changing the return signature of `findComponentMatching`, which got renamed to `findComponentDataMatching`. **Failing on Promise Rejects**: Turns out that if a Promise rejected due to an error or `Promise.reject` we were ignoring it and letting tests pass. Now, tests will Fail if any unhandled promise rejects. This uncovered a variety of errors throughout the test suite that had to be fixed. The most significant one was during the `theme-manager` tests when all packages (and their stores with async DB requests) was loaded. Long after the `theme-manager` specs finished, those DB requests were (somtimes) silently failing. **Globally stub `DatabaseStore._query`**: All tests shouldn't actually make queries on the database. Furthremore, the `inTransaction` block doesn't resolve at all unless `_query` is stubbed. Instead of manually remembering to do this in every test that touches the DB, it's now mocked in `spec_helper`. This broke a handful of tests that needed to be manually fixed. **ESLint Fixes**: Some minor fixes to the linter config to prevent yelling about minor ES6 things and ensuring we have the correct parser. Test Plan: new tests Reviewers: drew, bengotow, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2419
2016-02-03 04:28:06 +08:00
import {
Task,
NylasAPI,
APIError,
Metadata,
DatabaseStore,
SyncbackModelTask,
DatabaseTransaction } from 'nylas-exports'
class TestTask extends SyncbackModelTask {
getModelConstructor() {
return Metadata
}
}
describe("SyncbackModelTask", () => {
beforeEach(() => {
this.testModel = new Metadata({accountId: 'account-123'})
spyOn(DatabaseTransaction.prototype, "persistModel")
spyOn(DatabaseStore, "findBy")
.andReturn(Promise.resolve(this.testModel));
feat(error): improve error reporting. Now `NylasEnv.reportError` Summary: The goal is to let us see what plugins are throwing errors on Sentry. We are using a Sentry `tag` to identify and group plugins and their errors. Along the way, I cleaned up the error catching and reporting system. There was a lot of duplicate error logic (that wasn't always right) and some legacy Atom error handling. Now, if you catch an error that we should report (like when handling extensions), call `NylasEnv.reportError`. This used to be called `emitError` but I changed it to `reportError` to be consistent with the ErrorReporter and be a bit more indicative of what it does. In the production version, the `ErrorLogger` will forward the request to the `nylas-private-error-reporter` which will report to Sentry. The `reportError` function also now inspects the stack to determine which plugin(s) it came from. These are passed along to Sentry. I also cleaned up the `console.log` and `console.error` code. We were logging errors multiple times making the console confusing to read. Worse is that we were logging the `error` object, which would print not the stack of the actual error, but rather the stack of where the console.error was logged from. Printing `error.stack` instead shows much more accurate stack traces. See changes in the Edgehill repo here: https://github.com/nylas/edgehill/commit/8c4a86eb7ee1a06249a9ae35397e2084a09ad1dc Test Plan: Manual Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2509
2016-02-04 07:06:52 +08:00
spyOn(NylasEnv, "reportError")
feat(metadata): add cloudState that sync with Metadata service Summary: Now all plugins get passed a `cloudState` object to their `activate` method. The `cloudState` object is an instance of `CloudState` and acts like a key-value store backed by the yet-to-be-implemented Metadata service. It has a `get`, `getAll`, and `observe` method. The `observe` method returns a new `Rx.Observable` for the given key. It has a `set`, and `unset` method that doesn't actually mutate state, but rather dispatches new `Task`s to Create, Update, and Delete `Metadata` objects. The whole object is backed by `Metadata` objects. Since these are standard Database Objects that will appear on the delta sync streaming API, any updates from the server will automatically propagate down to listening views via the `Rx.Observable`s. Additionally, there is a new `N1-Send-Later` stub plugin that demonstrates how to use the `cloudState`. There are few other minor refactors included in this diff: **Generic CUD Tasks**: There is now a generic `CreateModelTask`, `UpdateModelTask`, and `DestroyModelTask`. These can either be used as-is or trivially overridden to easily update simple objects. Hopefully all of the boilerplate rollback, error handling, and undo logic won't have to be re-duplicated on every task. There are also tests for these tasks. We use them to perform mutating actions on `Metadata` objects. **New `boundProps` for `InjectedComponents`**: When making the `N1-Send_later` plugin, I realized that the injected component needed to get the `cloudState` somehow. Traditionally components would require Stores and load data that way, but these are setup at `require`-time. Now that `cloudState` only is available on `activate` we needed a way to get the data to the components. There's now the concept of `boundProps` which will be props added to the Component when it gets injected. This required changing the return signature of `findComponentMatching`, which got renamed to `findComponentDataMatching`. **Failing on Promise Rejects**: Turns out that if a Promise rejected due to an error or `Promise.reject` we were ignoring it and letting tests pass. Now, tests will Fail if any unhandled promise rejects. This uncovered a variety of errors throughout the test suite that had to be fixed. The most significant one was during the `theme-manager` tests when all packages (and their stores with async DB requests) was loaded. Long after the `theme-manager` specs finished, those DB requests were (somtimes) silently failing. **Globally stub `DatabaseStore._query`**: All tests shouldn't actually make queries on the database. Furthremore, the `inTransaction` block doesn't resolve at all unless `_query` is stubbed. Instead of manually remembering to do this in every test that touches the DB, it's now mocked in `spec_helper`. This broke a handful of tests that needed to be manually fixed. **ESLint Fixes**: Some minor fixes to the linter config to prevent yelling about minor ES6 things and ensuring we have the correct parser. Test Plan: new tests Reviewers: drew, bengotow, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2419
2016-02-03 04:28:06 +08:00
spyOn(NylasAPI, "makeRequest").andReturn(Promise.resolve({
version: 10,
id: "server-123",
}))
});
const performRemote = (fn) => {
window.waitsForPromise(() => {
return this.task.performRemote().then(fn)
});
}
describe("performLocal", () => {
it("throws if basic fields are missing", () => {
const t = new SyncbackModelTask()
try {
t.performLocal()
throw new Error("Shouldn't succeed");
} catch (e) {
expect(e.message).toMatch(/^Must pass.*/)
}
});
});
describe("performRemote", () => {
beforeEach(() => {
this.task = new TestTask({
clientId: "local-123",
endpoint: "/test",
})
});
it("fetches the latest model", () => {
spyOn(this.task, "getLatestModel").andCallThrough()
spyOn(this.task, "verifyModel").andCallThrough()
performRemote(() => {
expect(this.task.getLatestModel).toHaveBeenCalled()
const model = this.task.verifyModel.calls[0].args[0]
expect(model).toBe(this.testModel)
})
});
it("throws an error if getLatestModel hasn't been implemented", () => {
const bumTask = new SyncbackModelTask({clientId: 'local-123'});
spyOn(this.task, "getModelConstructor").andCallThrough()
window.waitsForPromise(() => {
return bumTask.performRemote().then((err) => {
expect(err[0]).toBe(Task.Status.Failed)
expect(err[1].message).toMatch(/must subclass/)
})
});
});
it("verifies the model", () => {
spyOn(this.task, "verifyModel").andCallThrough()
spyOn(this.task, "makeRequest").andCallThrough()
performRemote(() => {
expect(this.task.verifyModel).toHaveBeenCalled()
const model = this.task.makeRequest.calls[0].args[0]
expect(model).toBe(this.testModel)
})
});
it("gets the correct path and method for existing objects", () => {
jasmine.unspy(DatabaseStore, "findBy")
const serverModel = new Metadata({localId: 'local-123', serverId: 'server-123'})
spyOn(DatabaseStore, "findBy").andReturn(Promise.resolve(serverModel));
spyOn(this.task, "getPathAndMethod").andCallThrough();
performRemote(() => {
expect(this.task.getPathAndMethod).toHaveBeenCalled()
const opts = NylasAPI.makeRequest.calls[0].args[0]
expect(opts.path).toBe("/test/server-123")
expect(opts.method).toBe("PUT")
});
});
it("gets the correct path and method for new objects", () => {
spyOn(this.task, "getPathAndMethod").andCallThrough();
performRemote(() => {
expect(this.task.getPathAndMethod).toHaveBeenCalled()
const opts = NylasAPI.makeRequest.calls[0].args[0]
expect(opts.path).toBe("/test")
expect(opts.method).toBe("POST")
});
});
it("lets tasks override path and method", () => {
class TaskMethodAndPath extends SyncbackModelTask {
getModelConstructor() {
return Metadata
}
getPathAndMethod = () => {
return {
path: `/override`,
method: "DELETE",
}
}
}
const task = new TaskMethodAndPath({clientId: 'local-123'});
spyOn(task, "getPathAndMethod").andCallThrough();
spyOn(task, "getModelConstructor").andCallThrough()
window.waitsForPromise(() => {
return task.performRemote().then(() => {
expect(task.getPathAndMethod).toHaveBeenCalled()
const opts = NylasAPI.makeRequest.calls[0].args[0]
expect(opts.path).toBe("/override")
expect(opts.method).toBe("DELETE")
})
});
});
it("makes a request with the correct data", () => {
spyOn(this.task, "makeRequest").andCallThrough();
// So it doesn't get changed by the time we inspect it
spyOn(this.task, "updateLocalModel").andReturn(Promise.resolve())
performRemote(() => {
expect(this.task.makeRequest).toHaveBeenCalled()
const opts = NylasAPI.makeRequest.calls[0].args[0]
expect(opts.path).toBe("/test")
expect(opts.method).toBe("POST")
expect(opts.accountId).toBe("account-123")
expect(opts.returnsModel).toBe(false)
expect(opts.body).toEqual(this.testModel.toJSON())
});
});
it("updates the local model with only the version and serverId", () => {
spyOn(this.task, "updateLocalModel").andCallThrough()
performRemote(() => {
expect(this.task.updateLocalModel).toHaveBeenCalled();
const opts = this.task.updateLocalModel.calls[0].args[0]
expect(opts.version).toBe(10)
expect(opts.id).toBe("server-123")
expect(DatabaseTransaction.prototype.persistModel).toHaveBeenCalled()
const model = DatabaseTransaction.prototype.persistModel.calls[0].args[0]
expect(model.serverId).toBe('server-123')
expect(model.version).toBe(10)
});
});
it("retries on retry-able API errors", () => {
jasmine.unspy(NylasAPI, "makeRequest");
const err = new APIError({statusCode: 429});
spyOn(NylasAPI, "makeRequest").andReturn(Promise.reject(err))
performRemote((status) => {
expect(status).toBe(Task.Status.Retry)
});
});
it("failes on permanent errors", () => {
jasmine.unspy(NylasAPI, "makeRequest");
const err = new APIError({statusCode: 500});
spyOn(NylasAPI, "makeRequest").andReturn(Promise.reject(err))
performRemote((status) => {
expect(status[0]).toBe(Task.Status.Failed)
expect(status[1].statusCode).toBe(500)
});
});
it("fails and notifies us on other types of errors", () => {
const errMsg = "This is a test error"
spyOn(this.task, "updateLocalModel").andCallFake(() => {
throw new Error(errMsg)
})
performRemote((status) => {
expect(status[0]).toBe(Task.Status.Failed)
expect(status[1].message).toBe(errMsg)
feat(error): improve error reporting. Now `NylasEnv.reportError` Summary: The goal is to let us see what plugins are throwing errors on Sentry. We are using a Sentry `tag` to identify and group plugins and their errors. Along the way, I cleaned up the error catching and reporting system. There was a lot of duplicate error logic (that wasn't always right) and some legacy Atom error handling. Now, if you catch an error that we should report (like when handling extensions), call `NylasEnv.reportError`. This used to be called `emitError` but I changed it to `reportError` to be consistent with the ErrorReporter and be a bit more indicative of what it does. In the production version, the `ErrorLogger` will forward the request to the `nylas-private-error-reporter` which will report to Sentry. The `reportError` function also now inspects the stack to determine which plugin(s) it came from. These are passed along to Sentry. I also cleaned up the `console.log` and `console.error` code. We were logging errors multiple times making the console confusing to read. Worse is that we were logging the `error` object, which would print not the stack of the actual error, but rather the stack of where the console.error was logged from. Printing `error.stack` instead shows much more accurate stack traces. See changes in the Edgehill repo here: https://github.com/nylas/edgehill/commit/8c4a86eb7ee1a06249a9ae35397e2084a09ad1dc Test Plan: Manual Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2509
2016-02-04 07:06:52 +08:00
expect(NylasEnv.reportError).toHaveBeenCalled()
feat(metadata): add cloudState that sync with Metadata service Summary: Now all plugins get passed a `cloudState` object to their `activate` method. The `cloudState` object is an instance of `CloudState` and acts like a key-value store backed by the yet-to-be-implemented Metadata service. It has a `get`, `getAll`, and `observe` method. The `observe` method returns a new `Rx.Observable` for the given key. It has a `set`, and `unset` method that doesn't actually mutate state, but rather dispatches new `Task`s to Create, Update, and Delete `Metadata` objects. The whole object is backed by `Metadata` objects. Since these are standard Database Objects that will appear on the delta sync streaming API, any updates from the server will automatically propagate down to listening views via the `Rx.Observable`s. Additionally, there is a new `N1-Send-Later` stub plugin that demonstrates how to use the `cloudState`. There are few other minor refactors included in this diff: **Generic CUD Tasks**: There is now a generic `CreateModelTask`, `UpdateModelTask`, and `DestroyModelTask`. These can either be used as-is or trivially overridden to easily update simple objects. Hopefully all of the boilerplate rollback, error handling, and undo logic won't have to be re-duplicated on every task. There are also tests for these tasks. We use them to perform mutating actions on `Metadata` objects. **New `boundProps` for `InjectedComponents`**: When making the `N1-Send_later` plugin, I realized that the injected component needed to get the `cloudState` somehow. Traditionally components would require Stores and load data that way, but these are setup at `require`-time. Now that `cloudState` only is available on `activate` we needed a way to get the data to the components. There's now the concept of `boundProps` which will be props added to the Component when it gets injected. This required changing the return signature of `findComponentMatching`, which got renamed to `findComponentDataMatching`. **Failing on Promise Rejects**: Turns out that if a Promise rejected due to an error or `Promise.reject` we were ignoring it and letting tests pass. Now, tests will Fail if any unhandled promise rejects. This uncovered a variety of errors throughout the test suite that had to be fixed. The most significant one was during the `theme-manager` tests when all packages (and their stores with async DB requests) was loaded. Long after the `theme-manager` specs finished, those DB requests were (somtimes) silently failing. **Globally stub `DatabaseStore._query`**: All tests shouldn't actually make queries on the database. Furthremore, the `inTransaction` block doesn't resolve at all unless `_query` is stubbed. Instead of manually remembering to do this in every test that touches the DB, it's now mocked in `spec_helper`. This broke a handful of tests that needed to be manually fixed. **ESLint Fixes**: Some minor fixes to the linter config to prevent yelling about minor ES6 things and ensuring we have the correct parser. Test Plan: new tests Reviewers: drew, bengotow, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2419
2016-02-03 04:28:06 +08:00
});
});
});
describe("undo/redo", () => {
it("cant be undone", () => {
expect(this.task.canBeUndone()).toBe(false)
});
it("isn't an undo task", () => {
expect(this.task.isUndo()).toBe(false)
});
});
});