mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-12 04:25:31 +08:00
74e21bce16
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
239 lines
10 KiB
CoffeeScript
239 lines
10 KiB
CoffeeScript
_ = require 'underscore'
|
|
|
|
Label = require '../../src/flux/models/label'
|
|
Thread = require '../../src/flux/models/thread'
|
|
TestModel = require '../fixtures/db-test-model'
|
|
ModelQuery = require '../../src/flux/models/query'
|
|
DatabaseStore = require '../../src/flux/stores/database-store'
|
|
|
|
testMatchers = {'id': 'b'}
|
|
testModelInstance = new TestModel(id: "1234")
|
|
testModelInstanceA = new TestModel(id: "AAA")
|
|
testModelInstanceB = new TestModel(id: "BBB")
|
|
|
|
describe "DatabaseStore", ->
|
|
beforeEach ->
|
|
TestModel.configureBasic()
|
|
|
|
DatabaseStore._atomicallyQueue = undefined
|
|
DatabaseStore._mutationQueue = undefined
|
|
DatabaseStore._inTransaction = false
|
|
|
|
spyOn(ModelQuery.prototype, 'where').andCallThrough()
|
|
spyOn(DatabaseStore, 'accumulateAndTrigger').andCallFake -> Promise.resolve()
|
|
|
|
@performed = []
|
|
|
|
# Note: We spy on _query and test all of the convenience methods that sit above
|
|
# it. None of these tests evaluate whether _query works!
|
|
jasmine.unspy(DatabaseStore, "_query")
|
|
spyOn(DatabaseStore, "_query").andCallFake (query, values=[], options={}) =>
|
|
@performed.push({query: query, values: values})
|
|
return Promise.resolve([])
|
|
|
|
describe "find", ->
|
|
it "should return a ModelQuery for retrieving a single item by Id", ->
|
|
q = DatabaseStore.find(TestModel, "4")
|
|
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = '4' LIMIT 1")
|
|
|
|
describe "findBy", ->
|
|
it "should pass the provided predicates on to the ModelQuery", ->
|
|
matchers = {'id': 'b'}
|
|
DatabaseStore.findBy(TestModel, testMatchers)
|
|
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers)
|
|
|
|
it "should return a ModelQuery ready to be executed", ->
|
|
q = DatabaseStore.findBy(TestModel, testMatchers)
|
|
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' LIMIT 1")
|
|
|
|
describe "findAll", ->
|
|
it "should pass the provided predicates on to the ModelQuery", ->
|
|
DatabaseStore.findAll(TestModel, testMatchers)
|
|
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers)
|
|
|
|
it "should return a ModelQuery ready to be executed", ->
|
|
q = DatabaseStore.findAll(TestModel, testMatchers)
|
|
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' ")
|
|
|
|
describe "modelify", ->
|
|
beforeEach ->
|
|
@models = [
|
|
new Thread(clientId: 'local-A'),
|
|
new Thread(clientId: 'local-B'),
|
|
new Thread(clientId: 'local-C'),
|
|
new Thread(clientId: 'local-D', serverId: 'SERVER:D'),
|
|
new Thread(clientId: 'local-E', serverId: 'SERVER:E'),
|
|
new Thread(clientId: 'local-F', serverId: 'SERVER:F'),
|
|
new Thread(clientId: 'local-G', serverId: 'SERVER:G')
|
|
]
|
|
# Actually returns correct sets for queries, since matchers can evaluate
|
|
# themselves against models in memory
|
|
spyOn(DatabaseStore, 'run').andCallFake (query) =>
|
|
results = []
|
|
for model in @models
|
|
found = _.every query._matchers, (matcher) ->
|
|
matcher.evaluate(model)
|
|
results.push(model) if found
|
|
Promise.resolve(results)
|
|
|
|
describe "when given an array or input that is not an array", ->
|
|
it "resolves immediately with an empty array", ->
|
|
waitsForPromise =>
|
|
DatabaseStore.modelify(Thread, null).then (output) =>
|
|
expect(output).toEqual([])
|
|
|
|
describe "when given an array of mixed IDs, clientIDs, and models", ->
|
|
it "resolves with an array of models", ->
|
|
input = ['SERVER:F', 'local-B', 'local-C', 'SERVER:D', @models[6]]
|
|
expectedOutput = [@models[5], @models[1], @models[2], @models[3], @models[6]]
|
|
waitsForPromise =>
|
|
DatabaseStore.modelify(Thread, input).then (output) =>
|
|
expect(output).toEqual(expectedOutput)
|
|
|
|
describe "when the input is only IDs", ->
|
|
it "resolves with an array of models", ->
|
|
input = ['SERVER:D', 'SERVER:F', 'SERVER:G']
|
|
expectedOutput = [@models[3], @models[5], @models[6]]
|
|
waitsForPromise =>
|
|
DatabaseStore.modelify(Thread, input).then (output) =>
|
|
expect(output).toEqual(expectedOutput)
|
|
|
|
describe "when the input is only clientIDs", ->
|
|
it "resolves with an array of models", ->
|
|
input = ['local-A', 'local-B', 'local-C', 'local-D']
|
|
expectedOutput = [@models[0], @models[1], @models[2], @models[3]]
|
|
waitsForPromise =>
|
|
DatabaseStore.modelify(Thread, input).then (output) =>
|
|
expect(output).toEqual(expectedOutput)
|
|
|
|
describe "when the input is all models", ->
|
|
it "resolves with an array of models", ->
|
|
input = [@models[0], @models[1], @models[2], @models[3]]
|
|
expectedOutput = [@models[0], @models[1], @models[2], @models[3]]
|
|
waitsForPromise =>
|
|
DatabaseStore.modelify(Thread, input).then (output) =>
|
|
expect(output).toEqual(expectedOutput)
|
|
|
|
describe "count", ->
|
|
it "should pass the provided predicates on to the ModelQuery", ->
|
|
DatabaseStore.findAll(TestModel, testMatchers)
|
|
expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers)
|
|
|
|
it "should return a ModelQuery configured for COUNT ready to be executed", ->
|
|
q = DatabaseStore.findAll(TestModel, testMatchers)
|
|
expect(q.sql()).toBe("SELECT `TestModel`.`data` FROM `TestModel` WHERE `TestModel`.`id` = 'b' ")
|
|
|
|
describe "inTransaction", ->
|
|
it "calls the provided function inside an exclusive transaction", ->
|
|
waitsForPromise =>
|
|
DatabaseStore.inTransaction( =>
|
|
DatabaseStore._query("TEST")
|
|
).then =>
|
|
expect(@performed.length).toBe 3
|
|
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[1].query).toBe "TEST"
|
|
expect(@performed[2].query).toBe "COMMIT"
|
|
|
|
it "preserves resolved values", ->
|
|
waitsForPromise =>
|
|
DatabaseStore.inTransaction( =>
|
|
DatabaseStore._query("TEST")
|
|
return Promise.resolve("myValue")
|
|
).then (myValue) =>
|
|
expect(myValue).toBe "myValue"
|
|
|
|
it "always fires a COMMIT, even if the body function fails", ->
|
|
waitsForPromise =>
|
|
DatabaseStore.inTransaction( =>
|
|
throw new Error("BOOO")
|
|
).catch =>
|
|
expect(@performed.length).toBe 2
|
|
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[1].query).toBe "COMMIT"
|
|
|
|
it "can be called multiple times and get queued", ->
|
|
waitsForPromise =>
|
|
Promise.all([
|
|
DatabaseStore.inTransaction( -> )
|
|
DatabaseStore.inTransaction( -> )
|
|
DatabaseStore.inTransaction( -> )
|
|
]).then =>
|
|
expect(@performed.length).toBe 6
|
|
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[1].query).toBe "COMMIT"
|
|
expect(@performed[2].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[3].query).toBe "COMMIT"
|
|
expect(@performed[4].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[5].query).toBe "COMMIT"
|
|
|
|
it "carries on if one of them fails, but still calls the COMMIT for the failed block", ->
|
|
caughtError = false
|
|
DatabaseStore.inTransaction( => DatabaseStore._query("ONE") )
|
|
DatabaseStore.inTransaction( => throw new Error("fail") ).catch ->
|
|
caughtError = true
|
|
DatabaseStore.inTransaction( => DatabaseStore._query("THREE") )
|
|
advanceClock(100)
|
|
expect(@performed.length).toBe 8
|
|
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[1].query).toBe "ONE"
|
|
expect(@performed[2].query).toBe "COMMIT"
|
|
expect(@performed[3].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[4].query).toBe "COMMIT"
|
|
expect(@performed[5].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[6].query).toBe "THREE"
|
|
expect(@performed[7].query).toBe "COMMIT"
|
|
expect(caughtError).toBe true
|
|
|
|
it "is actually running in series and blocks on never-finishing specs", ->
|
|
resolver = null
|
|
DatabaseStore.inTransaction( -> )
|
|
advanceClock(100)
|
|
expect(@performed.length).toBe 2
|
|
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[1].query).toBe "COMMIT"
|
|
DatabaseStore.inTransaction( -> new Promise (resolve, reject) -> resolver = resolve)
|
|
advanceClock(100)
|
|
blockedPromiseDone = false
|
|
DatabaseStore.inTransaction( -> ).then =>
|
|
blockedPromiseDone = true
|
|
advanceClock(100)
|
|
expect(@performed.length).toBe 3
|
|
expect(@performed[2].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(blockedPromiseDone).toBe false
|
|
|
|
# Now that we've made our assertion about blocking, we need to clean up
|
|
# our test and actually resolve that blocked promise now, otherwise
|
|
# remaining tests won't run properly.
|
|
advanceClock(100)
|
|
resolver()
|
|
advanceClock(100)
|
|
expect(blockedPromiseDone).toBe true
|
|
advanceClock(100)
|
|
|
|
it "can be called multiple times and preserve return values", ->
|
|
waitsForPromise =>
|
|
v1 = null
|
|
v2 = null
|
|
v3 = null
|
|
Promise.all([
|
|
DatabaseStore.inTransaction( -> "a" ).then (val) -> v1 = val
|
|
DatabaseStore.inTransaction( -> "b" ).then (val) -> v2 = val
|
|
DatabaseStore.inTransaction( -> "c" ).then (val) -> v3 = val
|
|
]).then =>
|
|
expect(v1).toBe "a"
|
|
expect(v2).toBe "b"
|
|
expect(v3).toBe "c"
|
|
|
|
it "can be called multiple times and get queued", ->
|
|
waitsForPromise =>
|
|
DatabaseStore.inTransaction( -> )
|
|
.then -> DatabaseStore.inTransaction( -> )
|
|
.then -> DatabaseStore.inTransaction( -> )
|
|
.then =>
|
|
expect(@performed.length).toBe 6
|
|
expect(@performed[0].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[1].query).toBe "COMMIT"
|
|
expect(@performed[2].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[3].query).toBe "COMMIT"
|
|
expect(@performed[4].query).toBe "BEGIN IMMEDIATE TRANSACTION"
|
|
expect(@performed[5].query).toBe "COMMIT"
|