test: add IndexedDB tests (#1075)

* test: add IndexedDB tests

Adds unit tests using fake-indexeddb.

* remove wtfnode dep
This commit is contained in:
Nolan Lawson 2019-03-03 18:34:10 -08:00 committed by GitHub
parent 93a3e85994
commit 5cde48c2c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 311 additions and 9 deletions

View File

@ -103,6 +103,7 @@
"devDependencies": {
"assert": "^1.4.1",
"eslint-plugin-html": "^5.0.3",
"fake-indexeddb": "^2.0.5",
"mocha": "^6.0.2",
"now": "^14.0.1",
"standard": "^12.0.1",

View File

@ -42,6 +42,12 @@ function getOrCreateInstanceCache (cache, instanceName) {
export function clearCache (cache, instanceName) {
delete cache.caches[instanceName]
}
export function clearAllCaches (instanceName) {
let allCaches = [statusesCache, accountsCache, relationshipsCache, metaCache, notificationsCache]
for (let cache of allCaches) {
clearCache(cache, instanceName)
}
}
export function setInCache (cache, instanceName, key, value) {
let instanceCache = getOrCreateInstanceCache(cache, instanceName)
return instanceCache.set(key, value)

View File

@ -15,9 +15,10 @@ import { mark, stop } from '../_utils/marks'
import { deleteAll } from './utils'
import { createPinnedStatusKeyRange, createThreadKeyRange } from './keys'
import { getKnownInstances } from './knownInstances'
import noop from 'lodash-es/noop'
const BATCH_SIZE = 20
const TIME_AGO = 5 * 24 * 60 * 60 * 1000 // five days ago
export const TIME_AGO = 5 * 24 * 60 * 60 * 1000 // five days ago
const DELAY = 5 * 60 * 1000 // five minutes
function batchedGetAll (callGetAll, callback) {
@ -97,7 +98,7 @@ function cleanupRelationships (relationshipsStore, cutoff) {
)
}
async function cleanup (instanceName) {
export async function cleanup (instanceName) {
console.log('cleanup', instanceName)
mark(`cleanup:${instanceName}`)
let db = await getDatabase(instanceName)
@ -146,4 +147,5 @@ async function scheduledCleanup () {
}
}
export const scheduleCleanup = debounce(scheduledCleanup, DELAY)
// we have unit tests that test indexedDB; we don't want this thing to run forever
export const scheduleCleanup = process.browser ? debounce(scheduledCleanup, DELAY) : noop

View File

@ -17,4 +17,7 @@ export const USERNAME_LOWERCASE = '__pinafore_acct_lc'
export const DB_VERSION_INITIAL = 9
export const DB_VERSION_SEARCH_ACCOUNTS = 10
export const DB_VERSION_SNOWFLAKE_IDS = 11
export const DB_VERSION_CURRENT = 11
// Using an object for these so that unit tests can change them
export const DB_VERSION_CURRENT = { version: 11 }
export const CURRENT_TIME = { now: () => Date.now() }

View File

@ -1,13 +1,14 @@
import { DB_VERSION_CURRENT } from './constants'
import { addKnownInstance, deleteKnownInstance } from './knownInstances'
import { migrations } from './migrations'
import { clearAllCaches } from './cache'
const openReqs = {}
const databaseCache = {}
function createDatabase (instanceName) {
return new Promise((resolve, reject) => {
let req = indexedDB.open(instanceName, DB_VERSION_CURRENT)
let req = indexedDB.open(instanceName, DB_VERSION_CURRENT.version)
openReqs[instanceName] = req
req.onerror = reject
req.onblocked = () => {
@ -73,4 +74,17 @@ export function deleteDatabase (instanceName) {
req.onerror = () => reject(req.error)
req.onblocked = () => console.error(`database ${instanceName} blocked`)
}).then(() => deleteKnownInstance(instanceName))
.then(() => clearAllCaches(instanceName))
}
// this should probably only be used in unit tests
export function closeDatabase (instanceName) {
// close any open requests
let openReq = openReqs[instanceName]
if (openReq && openReq.result) {
openReq.result.close()
}
delete openReqs[instanceName]
delete databaseCache[instanceName]
clearAllCaches(instanceName)
}

View File

@ -1,6 +1,8 @@
import { dbPromise, getDatabase } from './databaseLifecycle'
import { getInCache, hasInCache, setInCache } from './cache'
import { ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE } from './constants'
import {
ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE, CURRENT_TIME
} from './constants'
export async function getGenericEntityWithId (store, cache, instanceName, id) {
if (hasInCache(cache, instanceName, id)) {
@ -50,6 +52,6 @@ export function cloneForStorage (obj) {
break
}
}
res[TIMESTAMP] = Date.now()
res[TIMESTAMP] = CURRENT_TIME.now()
return res
}

5
tests/indexedDBShims.js Normal file
View File

@ -0,0 +1,5 @@
import indexedDB from 'fake-indexeddb'
import IDBKeyRange from 'fake-indexeddb/build/FDBKeyRange'
global.indexedDB = indexedDB
global.IDBKeyRange = IDBKeyRange

192
tests/unit/test-database.js Normal file
View File

@ -0,0 +1,192 @@
/* global it describe beforeEach afterEach */
import '../indexedDBShims'
import assert from 'assert'
import { closeDatabase, deleteDatabase, getDatabase } from '../../src/routes/_database/databaseLifecycle'
import * as dbApi from '../../src/routes/_database/databaseApis'
import times from 'lodash-es/times'
import cloneDeep from 'lodash-es/cloneDeep'
import {
TIMESTAMP, ACCOUNT_ID, STATUS_ID, REBLOG_ID, USERNAME_LOWERCASE,
CURRENT_TIME, DB_VERSION_CURRENT, DB_VERSION_SEARCH_ACCOUNTS, DB_VERSION_SNOWFLAKE_IDS
} from '../../src/routes/_database/constants'
import { cleanup, TIME_AGO } from '../../src/routes/_database/cleanup'
const INSTANCE_NAME = 'localhost:3000'
const INSTANCE_INFO = {
'uri': 'localhost:3000',
'title': 'lolcathost',
'description': 'blah',
'foo': {
'bar': true
}
}
const createStatus = i => ({
id: i.toString(),
created_at: new Date().toISOString(),
content: `Status #4{id}`,
account: {
id: '1'
}
})
const stripDBFields = item => {
let res = cloneDeep(item)
delete res[TIMESTAMP]
delete res[ACCOUNT_ID]
delete res[STATUS_ID]
delete res[REBLOG_ID]
delete res[USERNAME_LOWERCASE]
if (res.account) {
delete res.account[TIMESTAMP]
}
return res
}
describe('test-database.js', function () {
this.timeout(60000)
describe('db-basic', () => {
beforeEach(async () => {
await getDatabase(INSTANCE_NAME)
})
afterEach(async () => {
await deleteDatabase(INSTANCE_NAME)
})
it('basic indexeddb test', async () => {
let info = await dbApi.getInstanceInfo(INSTANCE_NAME)
assert(!info)
await dbApi.setInstanceInfo(INSTANCE_NAME, INSTANCE_INFO)
info = await dbApi.getInstanceInfo(INSTANCE_NAME)
assert.deepStrictEqual(info, INSTANCE_INFO)
})
it('basic indexeddb test 2', async () => {
// sanity check to make sure that we have a clean DB between each test
let info = await dbApi.getInstanceInfo(INSTANCE_NAME)
assert(!info)
await dbApi.setInstanceInfo(INSTANCE_NAME, INSTANCE_INFO)
info = await dbApi.getInstanceInfo(INSTANCE_NAME)
assert.deepStrictEqual(info, INSTANCE_INFO)
})
it('stores and sorts some statuses', async () => {
let allStatuses = times(40, createStatus)
await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', allStatuses)
let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20)
let expected = allStatuses.slice().reverse().slice(0, 20)
assert.deepStrictEqual(statuses.map(stripDBFields), expected)
statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', statuses[statuses.length - 1].id, 20)
expected = allStatuses.slice().reverse().slice(20, 40)
assert.deepStrictEqual(statuses.map(stripDBFields), expected)
})
it('cleans up old statuses', async () => {
// Pretend we are inserting a status from a long time ago. Note that we
// set a timestamp based on the *current* date when the status is inserted,
// not the date that the status was composed.
let longAgo = Date.now() - (TIME_AGO * 2)
let oldStatus = {
id: '1',
created_at: new Date(longAgo).toISOString(),
content: 'This is old',
account: {
id: '1'
}
}
let previousNow = CURRENT_TIME.now
CURRENT_TIME.now = () => longAgo
await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', [oldStatus])
CURRENT_TIME.now = previousNow
let newStatus = {
id: '2',
created_at: new Date().toISOString(),
content: 'This is new',
account: {
id: '2'
}
}
await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', [newStatus])
let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20)
assert.deepStrictEqual(statuses.map(stripDBFields), [newStatus, oldStatus])
let status1 = await dbApi.getStatus(INSTANCE_NAME, '1')
let status2 = await dbApi.getStatus(INSTANCE_NAME, '2')
assert.deepStrictEqual(stripDBFields(status1), oldStatus)
assert.deepStrictEqual(stripDBFields(status2), newStatus)
await cleanup(INSTANCE_NAME)
statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20)
assert.deepStrictEqual(statuses.map(stripDBFields), [newStatus])
status1 = await dbApi.getStatus(INSTANCE_NAME, '1')
status2 = await dbApi.getStatus(INSTANCE_NAME, '2')
assert(!!status1)
assert.deepStrictEqual(stripDBFields(status2), newStatus)
})
})
describe('db-migrations', () => {
let oldCurrentVersion
beforeEach(async () => {
oldCurrentVersion = DB_VERSION_CURRENT.version
})
afterEach(async () => {
DB_VERSION_CURRENT.version = oldCurrentVersion
await deleteDatabase(INSTANCE_NAME)
})
it('migrates from v10 to v11', async () => {
// open the db using the old version
DB_VERSION_CURRENT.version = DB_VERSION_SEARCH_ACCOUNTS
await getDatabase(INSTANCE_NAME)
// insert some statuses
let allStatuses = times(40, createStatus)
await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', allStatuses)
let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 40)
let expected = allStatuses.slice().reverse()
assert.deepStrictEqual(statuses.map(stripDBFields), expected)
// close the database
closeDatabase(INSTANCE_NAME)
// do a version upgrade
DB_VERSION_CURRENT.version = DB_VERSION_SNOWFLAKE_IDS
await getDatabase(INSTANCE_NAME)
// check that the old statuses are correct
statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 40)
expected = allStatuses.slice().reverse()
assert.deepStrictEqual(statuses.map(stripDBFields), expected)
// insert some more statuses for good measure
let moreStatuses = times(20, i => 40 + i).map(createStatus)
await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', moreStatuses)
statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 60)
expected = moreStatuses.slice().reverse().concat(allStatuses.reverse())
assert.deepStrictEqual(statuses.map(stripDBFields), expected)
})
})
})

View File

@ -1237,6 +1237,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-arraybuffer-es6@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.4.2.tgz#b567d364065843113589b6c1436bd9492701c7fe"
integrity sha512-HaJx92u12By863ZXVHZs4Bp1nkKaLpbs3Ec9SI1OKzq60Hz+Ks6z7UvdD8pIx61Ck3e8F9MH/IPEu5T0xKSbkQ==
base64-js@^1.0.2, base64-js@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
@ -2001,6 +2006,11 @@ core-js@^2.4.0, core-js@^2.5.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49"
integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ==
core-js@^2.4.1, core-js@^2.5.3:
version "2.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2447,6 +2457,13 @@ domelementtype@~1.1.1:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
dependencies:
webidl-conversions "^4.0.2"
domhandler@^2.3.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
@ -3023,6 +3040,15 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fake-indexeddb@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-2.0.5.tgz#829685232f79bcb9d182b8dd33934e9e5657ed18"
integrity sha512-C68kh3Ec3L6JZaTpRm6+TjY5AOs4bwtEOXazzb6733UL0F0jLR7j939e+TdlUmJdxumFQmXIzFhyLu5ZifQc5w==
dependencies:
core-js "^2.4.1"
realistic-structured-clone "^2.0.1"
setimmediate "^1.0.5"
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@ -4542,6 +4568,11 @@ lodash.mergewith@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@4.17.11, "lodash@4.6.1 || ^4.16.1", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
@ -6126,6 +6157,16 @@ readdirp@^2.0.0, readdirp@^2.2.1:
micromatch "^3.1.10"
readable-stream "^2.0.2"
realistic-structured-clone@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz#2f8ec225b1f9af20efc79ac96a09043704414959"
integrity sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg==
dependencies:
core-js "^2.5.3"
domexception "^1.0.1"
typeson "^5.8.2"
typeson-registry "^1.0.0-alpha.20"
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@ -6575,7 +6616,7 @@ set-value@^2.0.0:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@^1.0.4:
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@ -7455,6 +7496,13 @@ tough-cookie@~2.4.3:
psl "^1.1.24"
punycode "^1.4.1"
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
dependencies:
punycode "^2.1.0"
tree-kill@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a"
@ -7551,6 +7599,21 @@ typescript@^3.3.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6"
integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.26"
resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.26.tgz#d1f337584196c5d5d112ad981e0dbbd2ced30c30"
integrity sha512-R0wwXIYSiJMh+1XfvyUsCnEGVERoJcNrMl9e/ka30dJ+gQyh4/0NU9WHaqUm8oHtZzZYCz+A5fDRCiXYIq7H1Q==
dependencies:
base64-arraybuffer-es6 "0.4.2"
typeson "5.11.0"
uuid "3.3.2"
whatwg-url "7.0.0"
typeson@5.11.0, typeson@^5.8.2:
version "5.11.0"
resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.11.0.tgz#a8273f00050be9eeef974aaa04a0c95a394f821a"
integrity sha512-S5KtLzcU4dr4BXh8VuJDYugsRGsDQYlumCbrmwuAX1a1GNpbVYK4p9wluCIfTVPFvVyV6wRfExXX6Q1+YDItEQ==
uglify-js@3.4.x:
version "3.4.9"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
@ -7701,7 +7764,7 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@^3.3.2:
uuid@3.3.2, uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
@ -7749,6 +7812,11 @@ webauth@^1.1.0:
resolved "https://registry.yarnpkg.com/webauth/-/webauth-1.1.0.tgz#64704f6b8026986605bc3ca629952e6e26fdd100"
integrity sha1-ZHBPa4AmmGYFvDymKZUubib90QA=
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
webpack-bundle-analyzer@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.4.tgz#095638487a664162f19e3b2fb7e621b7002af4b8"
@ -7805,6 +7873,15 @@ webpack@^4.29.6:
watchpack "^1.5.0"
webpack-sources "^1.3.0"
whatwg-url@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"