Mailspring/spec/stores/draft-store-proxy-spec.coffee

262 lines
10 KiB
CoffeeScript
Raw Normal View History

Message = require '../../src/flux/models/message'
Actions = require '../../src/flux/actions'
DatabaseStore = require '../../src/flux/stores/database-store'
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
DatabaseTransaction = require '../../src/flux/stores/database-transaction'
DraftEditingSession = require '../../src/flux/stores/draft-editing-session'
DraftChangeSet = DraftEditingSession.DraftChangeSet
_ = require 'underscore'
describe "DraftChangeSet", ->
beforeEach ->
@triggerSpy = jasmine.createSpy('trigger')
@commitResolve = null
@commitResolves = []
@commitSpy = jasmine.createSpy('commit').andCallFake =>
new Promise (resolve, reject) =>
@commitResolves.push(resolve)
@commitResolve = resolve
@changeSet = new DraftChangeSet(@triggerSpy, @commitSpy)
@changeSet._pending =
subject: 'Change to subject line'
describe "teardown", ->
it "should remove all of the pending and saving changes", ->
@changeSet.teardown()
expect(@changeSet._saving).toEqual({})
expect(@changeSet._pending).toEqual({})
describe "add", ->
it "should mark that the draft is not pristine", ->
@changeSet.add(body: 'Hello World!')
expect(@changeSet._pending.pristine).toEqual(false)
it "should add the changes to the _pending set", ->
@changeSet.add(body: 'Hello World!')
expect(@changeSet._pending.body).toEqual('Hello World!')
describe "otherwise", ->
it "should commit after thirty seconds", ->
spyOn(@changeSet, 'commit')
@changeSet.add({body: 'Hello World!'})
expect(@changeSet.commit).not.toHaveBeenCalled()
advanceClock(31000)
expect(@changeSet.commit).toHaveBeenCalled()
describe "commit", ->
it "should resolve immediately if the pending set is empty", ->
@changeSet._pending = {}
waitsForPromise =>
@changeSet.commit().then =>
expect(@commitSpy).not.toHaveBeenCalled()
it "should move changes to the saving set", ->
pendingBefore = _.extend({}, @changeSet._pending)
expect(@changeSet._saving).toEqual({})
@changeSet.commit()
advanceClock()
expect(@changeSet._pending).toEqual({})
expect(@changeSet._saving).toEqual(pendingBefore)
it "should call the commit handler and then clear the saving set", ->
@changeSet.commit()
advanceClock()
expect(@changeSet._saving).not.toEqual({})
@commitResolve()
advanceClock()
expect(@changeSet._saving).toEqual({})
describe "concurrency", ->
it "the commit function should always run serially", ->
firstFulfilled = false
secondFulfilled = false
@changeSet._pending = {subject: 'A'}
@changeSet.commit().then =>
@changeSet._pending = {subject: 'B'}
firstFulfilled = true
@changeSet.commit().then =>
secondFulfilled = true
advanceClock()
expect(firstFulfilled).toBe(false)
expect(secondFulfilled).toBe(false)
@commitResolves[0]()
advanceClock()
expect(firstFulfilled).toBe(true)
expect(secondFulfilled).toBe(false)
@commitResolves[1]()
advanceClock()
expect(firstFulfilled).toBe(true)
expect(secondFulfilled).toBe(true)
describe "applyToModel", ->
it "should apply the saving and then the pending change sets, in that order", ->
@changeSet._saving = {subject: 'A', body: 'Basketb'}
@changeSet._pending = {body: 'Basketball'}
m = new Message()
@changeSet.applyToModel(m)
expect(m.subject).toEqual('A')
expect(m.body).toEqual('Basketball')
describe "DraftEditingSession", ->
describe "constructor", ->
it "should make a query to fetch the draft", ->
spyOn(DatabaseStore, 'run').andCallFake =>
new Promise (resolve, reject) =>
session = new DraftEditingSession('client-id')
expect(DatabaseStore.run).toHaveBeenCalled()
describe "when given a draft object", ->
beforeEach ->
spyOn(DatabaseStore, 'run')
@draft = new Message(draft: true, body: '123')
@session = new DraftEditingSession('client-id', @draft)
it "should not make a query for the draft", ->
expect(DatabaseStore.run).not.toHaveBeenCalled()
it "prepare should resolve without querying for the draft", ->
waitsForPromise => @session.prepare().then =>
expect(@session.draft()).toBeDefined()
expect(DatabaseStore.run).not.toHaveBeenCalled()
describe "teardown", ->
it "should mark the session as destroyed", ->
spyOn(DraftEditingSession.prototype, "prepare")
session = new DraftEditingSession('client-id')
session.teardown()
expect(session._destroyed).toEqual(true)
describe "prepare", ->
beforeEach ->
@draft = new Message(draft: true, body: '123', clientId: 'client-id')
spyOn(DraftEditingSession.prototype, "prepare")
@session = new DraftEditingSession('client-id')
spyOn(@session, '_setDraft').andCallThrough()
feat(tasks): add Create, Update, Destroy tasks plus spec & lint fixes Summary: 1. **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. 1. **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. 1. **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. 1. **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: bengotow, juan, drew Differential Revision: https://phab.nylas.com/D2419 Remove cloudState and N1-Send-Later
2016-01-05 08:39:14 +08:00
spyOn(DatabaseStore, 'run').andCallFake (modelQuery) =>
Promise.resolve(@draft)
jasmine.unspy(DraftEditingSession.prototype, "prepare")
it "should call setDraft with the retrieved draft", ->
waitsForPromise =>
@session.prepare().then =>
expect(@session._setDraft).toHaveBeenCalledWith(@draft)
it "should resolve with the DraftEditingSession", ->
waitsForPromise =>
@session.prepare().then (val) =>
expect(val).toBe(@session)
describe "error handling", ->
it "should reject if the draft session has already been destroyed", ->
@session._destroyed = true
waitsForPromise =>
@session.prepare().then =>
expect(false).toBe(true)
.catch (val) =>
expect(val instanceof Error).toBe(true)
it "should reject if the draft cannot be found", ->
@draft = null
waitsForPromise =>
@session.prepare().then =>
expect(false).toBe(true)
.catch (val) =>
expect(val instanceof Error).toBe(true)
describe "when a draft changes", ->
beforeEach ->
@draft = new Message(draft: true, clientId: 'client-id', body: 'A', subject: 'initial')
@session = new DraftEditingSession('client-id', @draft)
advanceClock()
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
spyOn(DatabaseTransaction.prototype, "persistModel").andReturn Promise.resolve()
spyOn(Actions, "queueTask").andReturn Promise.resolve()
it "should ignore the update unless it applies to the current draft", ->
spyOn(@session, 'trigger')
@session._onDraftChanged(objectClass: 'message', objects: [new Message()])
expect(@session.trigger).not.toHaveBeenCalled()
@session._onDraftChanged(objectClass: 'message', objects: [@draft])
expect(@session.trigger).toHaveBeenCalled()
it "should apply the update to the current draft", ->
updatedDraft = @draft.clone()
updatedDraft.subject = 'This is the new subject'
@session._onDraftChanged(objectClass: 'message', objects: [updatedDraft])
expect(@session.draft().subject).toEqual(updatedDraft.subject)
it "atomically commits changes", ->
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(@draft))
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
spyOn(DatabaseStore, 'inTransaction').andCallThrough()
@session.changes.add({body: "123"})
waitsForPromise =>
@session.changes.commit().then =>
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
expect(DatabaseStore.inTransaction).toHaveBeenCalled()
expect(DatabaseStore.inTransaction.calls.length).toBe 1
it "persist the applied changes", ->
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(@draft))
@session.changes.add({body: "123"})
waitsForPromise =>
@session.changes.commit().then =>
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
expect(DatabaseTransaction.prototype.persistModel).toHaveBeenCalled()
updated = DatabaseTransaction.prototype.persistModel.calls[0].args[0]
expect(updated.body).toBe "123"
# Note: Syncback temporarily disabled
#
# it "queues a SyncbackDraftTask", ->
# spyOn(DatabaseStore, "run").andReturn(Promise.resolve(@draft))
# @session.changes.add({body: "123"})
# waitsForPromise =>
# @session.changes.commit().then =>
# expect(Actions.queueTask).toHaveBeenCalled()
# task = Actions.queueTask.calls[0].args[0]
# expect(task.draftClientId).toBe "client-id"
it "doesn't queues a SyncbackDraftTask if no Syncback is passed", ->
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(@draft))
waitsForPromise =>
@session.changes.commit({noSyncback: true}).then =>
expect(Actions.queueTask).not.toHaveBeenCalled()
describe "when findBy does not return a draft", ->
it "continues and persists it's local draft reference, so it is resaved and draft editing can continue", ->
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(null))
@session.changes.add({body: "123"})
waitsForPromise =>
@session.changes.commit().then =>
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
expect(DatabaseTransaction.prototype.persistModel).toHaveBeenCalled()
updated = DatabaseTransaction.prototype.persistModel.calls[0].args[0]
expect(updated.body).toBe "123"
it "does nothing if the draft is marked as destroyed", ->
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(@draft))
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
spyOn(DatabaseStore, 'inTransaction').andCallThrough()
waitsForPromise =>
@session._destroyed = true
@session.changes.add({body: "123"})
@session.changes.commit().then =>
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
expect(DatabaseStore.inTransaction).not.toHaveBeenCalled()
describe "draft pristine body", ->
describe "when the draft given to the session is pristine", ->
it "should return the initial body", ->
pristineDraft = new Message(draft: true, body: 'Hiya', pristine: true, clientId: 'client-id')
updatedDraft = pristineDraft.clone()
updatedDraft.body = '123444'
updatedDraft.pristine = false
@session = new DraftEditingSession('client-id', pristineDraft)
@session._onDraftChanged(objectClass: 'message', objects: [updatedDraft])
expect(@session.draftPristineBody()).toBe('Hiya')
describe "when the draft given to the session is not pristine", ->
it "should return null", ->
dirtyDraft = new Message(draft: true, body: 'Hiya', pristine: false)
@session = new DraftEditingSession('client-id', dirtyDraft)
expect(@session.draftPristineBody()).toBe(null)