test(pouchdb-server): fix tests

This commit is contained in:
Christopher Astfalk
2020-09-18 03:31:34 +02:00
parent 4997369f2a
commit 4f63f6bbd3
7 changed files with 257 additions and 298 deletions
+3 -3
View File
@@ -7,7 +7,7 @@ const fetch = require('node-fetch')
const pouchErrors = require('pouchdb-errors')
const { v4: uuid } = require('uuid')
const { nano, usersDB } = require('./db')
const { nano, usersDB, getUserDbName } = require('./db')
const api = express.Router()
api.use(express.json())
@@ -115,7 +115,7 @@ api.put(
// TODO: send email
if (process.env.NODE_ENV === 'development') {
await nano.db.create('userdb-' + Buffer.from(userID).toString('hex'))
await nano.db.create(getUserDbName(userID))
}
res.type('application/vnd.api+json')
@@ -210,7 +210,7 @@ api.delete('/account', ...createAuthValidator(), async (req, res, next) => {
await usersDB.destroy(req.user._id, req.user._rev)
if (process.env.NODE_ENV === 'development') {
await nano.db.destroy('userdb-' + Buffer.from(req.user.name).toString('hex'))
await nano.db.destroy(getUserDbName(req.user.name))
}
res.status(204).send('')
+6
View File
@@ -5,3 +5,9 @@ const nano = require('nano')(process.env.COUCH_URL || 'http://localhost:5984')
exports.nano = nano
exports.usersDB = nano.db.use('_users')
exports.getUserDbName = getUserDbName
function getUserDbName (userId) {
return 'userdb-' + Buffer.from(userId).toString('hex')
}
+77 -119
View File
@@ -1,146 +1,104 @@
'use strict'
const storeData = {}
import PouchDB from 'pouchdb-browser'
import memoryAdapter from 'pouchdb-adapter-memory'
import hoodieAPI from 'pouchdb-hoodie-api'
import CryptoStore from 'hoodie-plugin-store-crypto'
const hoodie = {
account: {
on: function () {}
},
PouchDB.plugin(memoryAdapter)
PouchDB.plugin(hoodieAPI)
store: {
find: function (id) {
if (Array.isArray(id)) {
return Promise.all(
id.map(
aID => hoodie.store.find(aID).catch(() => ({ status: 404 }))
)
)
}
let db
const doc = storeData[id]
if (doc != null) {
return Promise.resolve(doc)
}
beforeEach(() => {
db = new PouchDB('test', { adapter: 'memory' })
})
const error = new Error('not found')
error.status = 404
return Promise.reject(error)
},
updateOrAdd: function (doc) {
if (Array.isArray(doc)) {
return Promise.all(doc.map(hoodie.store.updateOrAdd))
}
const newDoc = Object.assign(storeData[doc._id] || {}, doc, {
_rev: doc._rev != null && doc._rev.length > 0 ? doc._rev : '1-1234567890'
})
storeData[doc._id] = newDoc
return Promise.resolve(newDoc)
},
pull: function (ids) {
return Promise.all(ids.map(id => hoodie.store.find(id))).catch(() => [])
},
withIdPrefix: function (prefix) {
return {
findAll () {
const docs = []
for (const key in storeData) {
if (key.startsWith(prefix)) {
docs.push(storeData[key])
}
}
return Promise.resolve(docs)
}
}
},
on: function () {},
off: function () {},
one: function () {}
}
}
test('import is a function that adds the cryptoStore to hoodie', () => {
expect(hoodie.cryptoStore).toBeUndefined()
require('hoodie-plugin-store-crypto')(hoodie)
expect(hoodie.cryptoStore).toBeTruthy()
afterEach(async () => {
await db.destroy()
})
test('cryptoStore and its methods exists', () => {
expect(typeof hoodie.cryptoStore).toBe('object')
const cryptoStore = new CryptoStore(db.hoodieApi())
expect(typeof hoodie.cryptoStore.setup).toBe('function')
expect(typeof hoodie.cryptoStore.unlock).toBe('function')
expect(typeof hoodie.cryptoStore.changePassword).toBe('function')
expect(typeof cryptoStore).toBe('object')
expect(typeof hoodie.cryptoStore.add).toBe('function')
expect(typeof hoodie.cryptoStore.find).toBe('function')
expect(typeof hoodie.cryptoStore.findOrAdd).toBe('function')
expect(typeof hoodie.cryptoStore.findAll).toBe('function')
expect(typeof hoodie.cryptoStore.update).toBe('function')
expect(typeof hoodie.cryptoStore.updateOrAdd).toBe('function')
expect(typeof hoodie.cryptoStore.updateAll).toBe('function')
expect(typeof hoodie.cryptoStore.remove).toBe('function')
expect(typeof hoodie.cryptoStore.removeAll).toBe('function')
expect(typeof cryptoStore.setup).toBe('function')
expect(typeof cryptoStore.unlock).toBe('function')
expect(typeof cryptoStore.changePassword).toBe('function')
expect(typeof hoodie.cryptoStore.on).toBe('function')
expect(typeof hoodie.cryptoStore.off).toBe('function')
expect(typeof hoodie.cryptoStore.one).toBe('function')
expect(typeof cryptoStore.add).toBe('function')
expect(typeof cryptoStore.find).toBe('function')
expect(typeof cryptoStore.findOrAdd).toBe('function')
expect(typeof cryptoStore.findAll).toBe('function')
expect(typeof cryptoStore.update).toBe('function')
expect(typeof cryptoStore.updateOrAdd).toBe('function')
expect(typeof cryptoStore.updateAll).toBe('function')
expect(typeof cryptoStore.remove).toBe('function')
expect(typeof cryptoStore.removeAll).toBe('function')
expect(typeof hoodie.cryptoStore.withIdPrefix).toBe('function')
expect(typeof hoodie.cryptoStore.withPassword).toBe('function')
expect(typeof cryptoStore.on).toBe('function')
expect(typeof cryptoStore.off).toBe('function')
expect(typeof cryptoStore.one).toBe('function')
expect(typeof cryptoStore.withIdPrefix).toBe('function')
expect(typeof cryptoStore.withPassword).toBe('function')
})
test('cryptoStore requires to be unlocked', async () => {
await hoodie.cryptoStore.setup('testPassword')
await hoodie.cryptoStore.unlock('testPassword')
const cryptoStore = new CryptoStore(db.hoodieApi())
try {
await cryptoStore.add({ test: '' })
throw new Error('should have thrown')
} catch (err) {
expect(err.status).toBe(401)
}
await cryptoStore.setup('testPassword')
await cryptoStore.unlock('testPassword')
const added = await cryptoStore.add({ test: 'test' })
expect(added).toEqual({
_id: expect.any(String),
_rev: expect.any(String),
hoodie: {
createdAt: expect.any(String)
},
test: 'test'
})
})
test('cryptoStore encrypts documents', async () => {
let unencrypted = null
let callCount = 0
let date = null
const cryptoStore = new CryptoStore(db.hoodieApi())
hoodie.store.add = function (doc) {
callCount += 1
unencrypted = doc
await cryptoStore.setup('testPassword')
await cryptoStore.unlock('testPassword')
const result = Object.assign({}, doc, {
_rev: doc._rev != null && doc._rev.length > 0 ? doc._rev : '1-1234567890',
hoodie: {
created: new Date().toJSON()
}
})
date = result.hoodie.created
return Promise.resolve(result)
}
const result = await hoodie.cryptoStore.add({
const result = await cryptoStore.add({
test: 'object',
value: 2
})
expect(result.test).toBe('object')
expect(result.value).toBe(2)
expect(typeof result._id).toBe('string')
expect(typeof result._rev).toBe('string')
expect(result.hoodie.created).toBe(date)
expect(result).toEqual({
_id: expect.any(String),
_rev: expect.any(String),
hoodie: {
createdAt: expect.any(String)
},
test: 'object',
value: 2
})
expect(callCount).toBe(1)
expect(unencrypted._id).toBe(result._id)
expect(typeof unencrypted.data).toBe('string')
expect(typeof unencrypted.nonce).toBe('string')
expect(typeof unencrypted.tag).toBe('string')
expect(unencrypted.test).toBeUndefined()
expect(unencrypted.value).toBeUndefined()
const doc = await db.get(result._id)
expect(doc).toEqual({
_id: expect.any(String),
_rev: expect.any(String),
hoodie: {
createdAt: expect.any(String)
},
data: expect.any(String),
nonce: expect.any(String),
tag: expect.any(String)
})
})
+133 -109
View File
@@ -1,6 +1,11 @@
import nodeCrypto from 'crypto'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import { v4 } from 'uuid'
import PouchDB from 'pouchdb-browser'
import memoryAdapter from 'pouchdb-adapter-memory'
import hoodieApi from 'pouchdb-hoodie-api'
import { signInStatus } from '../bundles/account'
import {
@@ -14,9 +19,18 @@ import {
import AvatarName from '../avatarName'
PouchDB.plugin(memoryAdapter)
PouchDB.plugin(hoodieApi)
jest.mock('uuid')
v4.mockReturnValue('b039f51f-41d9-41e7-a4b1-5490fbfd5eb9')
window.TextEncoder = class TextEncoder {
encode (text) {
return Buffer.from(text)
}
}
it('didSignIn', () => {
const store = configureMockStore([thunk])()
@@ -90,10 +104,8 @@ it('saveAvatar', async () => {
const store = configureMockStore([
thunk.withExtraArgument({
hoodie: {
cryptoStore: {
withIdPrefix
}
cryptoStore: {
withIdPrefix
}
})
])({
@@ -154,19 +166,17 @@ it('loadSavedAvatars', async () => {
}))
let callback = null
const accountOne = jest.fn((event, fn) => {
const dbOne = jest.fn((event, fn) => {
callback = fn
})
const store = configureMockStore([
thunk.withExtraArgument({
hoodie: {
account: {
one: accountOne
},
cryptoStore: {
withIdPrefix
}
db: {
one: dbOne
},
cryptoStore: {
withIdPrefix
}
})
])({
@@ -208,9 +218,9 @@ it('loadSavedAvatars', async () => {
expect(withIdPrefix.mock.calls).toEqual([
['avatars/']
])
expect(accountOne.mock.calls.length).toBe(1)
expect(accountOne.mock.calls[0][0]).toBe('signout')
expect(accountOne.mock.calls[0][1]).toBeInstanceOf(Function)
expect(dbOne.mock.calls.length).toBe(1)
expect(dbOne.mock.calls[0][0]).toBe('destroyed')
expect(dbOne.mock.calls[0][1]).toBeInstanceOf(Function)
expect(on.mock.calls.length).toBe(1)
expect(on.mock.calls[0][0]).toBe('change')
@@ -286,10 +296,8 @@ it('saveGrid', async () => {
const store = configureMockStore([
thunk.withExtraArgument({
hoodie: {
cryptoStore: {
withIdPrefix
}
cryptoStore: {
withIdPrefix
}
})
])({
@@ -351,19 +359,17 @@ it('loadSavedGrids', async () => {
}))
let callback = null
const accountOne = jest.fn((event, fn) => {
const dbOne = jest.fn((event, fn) => {
callback = fn
})
const store = configureMockStore([
thunk.withExtraArgument({
hoodie: {
account: {
one: accountOne
},
cryptoStore: {
withIdPrefix
}
db: {
one: dbOne
},
cryptoStore: {
withIdPrefix
}
})
])({
@@ -401,9 +407,9 @@ it('loadSavedGrids', async () => {
expect(withIdPrefix.mock.calls).toEqual([
['grids/']
])
expect(accountOne.mock.calls.length).toBe(1)
expect(accountOne.mock.calls[0][0]).toBe('signout')
expect(accountOne.mock.calls[0][1]).toBeInstanceOf(Function)
expect(dbOne.mock.calls.length).toBe(1)
expect(dbOne.mock.calls[0][0]).toBe('destroyed')
expect(dbOne.mock.calls[0][1]).toBeInstanceOf(Function)
expect(on.mock.calls.length).toBe(1)
expect(on.mock.calls[0][0]).toBe('change')
@@ -467,38 +473,26 @@ it('loadSavedGrids', async () => {
it('should check sign in status with "isSignedIn"', async () => {
let result = null
const get = jest.fn(() => Promise.resolve(result))
let handler = null
const on = jest.fn((type, fn) => {
handler = fn
})
let callback = null
const one = jest.fn((event, fn) => {
callback = fn
})
const off = jest.fn()
const store = configureMockStore([thunk.withExtraArgument({
hoodie: {
account: {
get,
on,
one,
off
db: new PouchDB('localDB', { adapter: 'memory' }),
remoteDB: {
getSession: () => {
return Promise.resolve({
userCtx: { name: result }
})
}
}
})])()
result = {}
result = null
const isSignedInResultNotLoggedIn = await store.dispatch(isSignedIn())
expect(isSignedInResultNotLoggedIn).toBeFalsy()
result = {
session: 'sdkfgnsdnf',
username: 'tester.mactestface@viewer.com'
}
result = 'tester.mactestface@viewer.com'
const isSignedInResult = await store.dispatch(isSignedIn())
expect(isSignedInResult).toBeTruthy()
@@ -521,42 +515,6 @@ it('should check sign in status with "isSignedIn"', async () => {
}
}
])
store.clearActions()
expect(get.mock.calls).toEqual([
[
['session', 'username']
],
[
['session', 'username']
]
])
expect(on.mock.calls.length).toBe(1)
expect(on.mock.calls[0][0]).toBe('update')
const eventHandler = on.mock.calls[0][1]
expect(eventHandler).toBeInstanceOf(Function)
expect(one.mock.calls.length).toBe(1)
expect(one.mock.calls[0][0]).toBe('signout')
expect(off.mock.calls.length).toBe(0)
handler({ username: 'new.phone@whois.this' })
expect(store.getActions()).toEqual([
{
type: 'account/didUpdate',
payload: {
username: 'new.phone@whois.this'
}
}
])
callback()
expect(off.mock.calls).toEqual([
['update', eventHandler]
])
})
it('should unlock the app with "unlock"', async () => {
@@ -564,19 +522,79 @@ it('should unlock the app with "unlock"', async () => {
const findAll = jest.fn(() => Promise.resolve([]))
const on = jest.fn()
const one = jest.fn()
const logIn = jest.fn(() => Promise.resolve())
const sync = jest.fn(() => ({
on: () => {}
}))
let lastKeyObj = null
let lastHashFn = ''
let lastPw = ''
const importKey = jest.fn((type, pw, hashFn, exportable, arg) => {
expect(type).toBe('raw')
expect(exportable).toBeFalsy()
expect(arg).toEqual(['deriveBits'])
lastKeyObj = {}
lastHashFn = hashFn
lastPw = pw
return Promise.resolve(lastKeyObj)
})
let lastKey = null
const deriveBits = jest.fn((args, key, keyLength) => {
expect(key).toBe(lastKeyObj)
expect(args.name).toBe(lastHashFn)
expect(args.hash).toBe('SHA-512')
expect(keyLength).toBe(512)
if (args.name === 'PBKDF2') {
const hash = args.hash.toLowerCase().replace('-', '')
const key = nodeCrypto.pbkdf2Sync(lastPw, args.salt, args.iterations, keyLength, hash)
lastKey = key
return Promise.resolve(key)
} else if (args.name === 'HKDF') {
expect(lastPw).toBe(lastKey)
return Promise.resolve(Buffer.concat([
Buffer.alloc(32, 1),
Buffer.alloc(32, 2)
]))
} else {
throw new TypeError('unknown hash: ' + args.name)
}
})
window.crypto = {
subtle: {
importKey,
deriveBits
}
}
const store = configureMockStore([thunk.withExtraArgument({
hoodie: {
cryptoStore: {
unlock: unlockCryptoStore,
withIdPrefix: prefix => ({
findAll: findAll.bind(null, prefix),
on: on.bind(null, prefix)
})
db: {
get (id) {
if (id === '_local/account') {
return Promise.resolve({
_id: '_local/account',
accountId: 'a_id',
name: 'tester'
})
}
},
account: {
one
}
sync,
on,
one
},
remoteDB: {
close: () => {},
logIn
},
cryptoStore: {
unlock: unlockCryptoStore,
withIdPrefix: prefix => ({
findAll: findAll.bind(null, prefix),
on: on.bind(null, prefix)
})
}
})])({
account: {
@@ -599,17 +617,23 @@ it('should unlock the app with "unlock"', async () => {
}
])
expect(unlockCryptoStore.mock.calls).toEqual([
['password']
])
expect(findAll.mock.calls).toEqual([
['grids/'],
['avatars/']
])
expect(on.mock.calls.length).toBe(2)
expect(on.mock.calls[0][0]).toBe('grids/')
expect(on.mock.calls[1][0]).toBe('avatars/')
expect(one.mock.calls.length).toBe(2)
expect(one.mock.calls[0][0]).toBe('signout')
expect(one.mock.calls[1][0]).toBe('signout')
expect(findAll).toHaveBeenNthCalledWith(1, 'grids/')
expect(findAll).toHaveBeenNthCalledWith(2, 'avatars/')
expect(on).toBeCalledTimes(2)
expect(on).toHaveBeenNthCalledWith(1, 'grids/', 'change', expect.any(Function))
expect(on).toHaveBeenNthCalledWith(2, 'avatars/', 'change', expect.any(Function))
expect(one).toBeCalledTimes(2)
expect(one).toHaveBeenNthCalledWith(1, 'destroyed', expect.any(Function))
expect(one).toHaveBeenNthCalledWith(2, 'destroyed', expect.any(Function))
expect(logIn).toHaveBeenCalledWith(
'a_id',
'0101010101010101010101010101010101010101010101010101010101010101'
)
expect(unlockCryptoStore).toHaveBeenCalledWith(
'0202020202020202020202020202020202020202020202020202020202020202'
)
expect(sync).toBeCalled()
})
+3 -56
View File
@@ -95,7 +95,7 @@ describe('signUp', () => {
})
expect((await findByLabelText('Repeat password')).getAttribute('value'))
.toBe('secretPassword')
expect(queryByText('sign up').disabled).toBeTruthy()
expect(queryByText('sign up').disabled).toBeFalsy()
// Mismatch
fireEvent.change(queryByLabelText(/Password/), {
@@ -104,54 +104,15 @@ describe('signUp', () => {
}
})
expect(await findByText("Password doesn't match!")).toBeTruthy()
expect(queryByText('sign up').disabled).toBeTruthy()
fireEvent.change(queryByLabelText('Repeat password', { exact: false }), {
target: {
value: 'secretPassword2'
}
})
// Crypto password input
expect(await findByLabelText(/Encryption password/)).toBeTruthy()
expect(queryByText("Password doesn't match!")).toBeNull()
fireEvent.change(queryByLabelText(/Encryption password/), {
target: {
value: 'cryptoPassword'
}
})
expect((await findByLabelText(/^Encryption password/)).getAttribute('value'))
.toBe('cryptoPassword')
expect(queryByText('sign up').disabled).toBeTruthy()
// Crypto password 2 input
expect(queryByLabelText(/Repeat encryption password/)).toBeTruthy()
fireEvent.change(queryByLabelText(/Repeat encryption password/), {
target: {
value: 'cryptoPassword'
}
})
expect((await findByLabelText(/Repeat encryption password/)).getAttribute('value'))
.toBe('cryptoPassword')
expect(queryByText('sign up').disabled).toBeFalsy()
// Mismatch
fireEvent.change(queryByLabelText(/^Encryption password/), {
target: {
value: 'secretPassword'
}
})
expect(await findByText("Encryption password doesn't match!")).toBeTruthy()
expect(queryByText('sign up').disabled).toBeTruthy()
fireEvent.change(queryByLabelText(/Repeat encryption password/), {
target: {
value: 'secretPassword'
}
})
expect((await findByText('sign up')).disabled).toBeFalsy()
expect(queryByText("Encryption password doesn't match!")).toBeNull()
fireEvent.click(queryByText('sign up'))
expect(dispatch.mock.calls.length).toBe(1)
expect(dispatch.mock.calls[0][0]).toBeInstanceOf(Function)
@@ -234,25 +195,11 @@ describe('singIn', () => {
})
expect((await findByLabelText(/^Password/)).getAttribute('value'))
.toBe('secretPassword')
expect(queryByText('sign in').disabled).toBeTruthy()
expect(queryByText('sign in').disabled).toBeFalsy()
// Password 2 input
expect(queryByLabelText(/Repeat password/)).toBeNull()
// Crypto password input
expect(queryByLabelText(/Encryption password/)).toBeTruthy()
fireEvent.change(queryByLabelText(/Encryption password/), {
target: {
value: 'cryptoPassword'
}
})
expect((await findByLabelText(/Encryption password/)).getAttribute('value'))
.toBe('cryptoPassword')
expect(queryByText('sign in').disabled).toBeFalsy()
// Crypto password 2 input
expect(queryByLabelText(/Repeat encryption password/)).toBeNull()
fireEvent.click(queryByText('sign in'))
expect(dispatch.mock.calls.length).toBe(1)
expect(dispatch.mock.calls[0][0]).toBeInstanceOf(Function)
+30 -6
View File
@@ -23,7 +23,11 @@ const Container = ({ store }) => {
it('should render without crashing', () => {
const store = {
getState: () => ({}),
getState: () => ({
account: {
username: 'Tester MacTestface'
}
}),
dispatch: () => {},
subscribe: () => () => {}
}
@@ -34,7 +38,11 @@ it('should render without crashing', () => {
})
it('should unlock with return key', async () => {
const store = mockStore()
const store = mockStore({
account: {
username: 'Tester MacTestface'
}
})
const oldUnlockCallCount = unlock.mock.calls.length
@@ -63,7 +71,11 @@ it('should unlock with return key', async () => {
})
it('should unlock with unlock button clicked', async () => {
const store = mockStore()
const store = mockStore({
account: {
username: 'Tester MacTestface'
}
})
const oldUnlockCallCount = unlock.mock.calls.length
@@ -91,7 +103,11 @@ it('should unlock with unlock button clicked', async () => {
})
it('should call sign out when sign out button is clicked', async () => {
const store = mockStore()
const store = mockStore({
account: {
username: 'Tester MacTestface'
}
})
signOut.mockReturnValueOnce({ type: 'signOut' })
@@ -108,7 +124,11 @@ it('should call sign out when sign out button is clicked', async () => {
})
it('should show reset crypto password if the reset password button is clicked', async () => {
const store = mockStore()
const store = mockStore({
account: {
username: 'Tester MacTestface'
}
})
const { queryByText } = render(<Container store={store} />)
@@ -119,7 +139,11 @@ it('should show reset crypto password if the reset password button is clicked',
it('should pass aXe', async () => {
const store = {
getState: () => ({}),
getState: () => ({
account: {
username: 'Tester MacTestface'
}
}),
dispatch: () => {},
subscribe: () => () => {}
}
+5 -5
View File
@@ -2,7 +2,7 @@ import { proxyFetch, fetchLLSD } from './llsdFetch'
import LLSD, { UUID } from '../llsd'
describe('proxyFetch', () => {
it('should make a fetch request to /hoodie/andromeda-viewer/proxy/', async () => {
it('should make a fetch request to /api/proxy/', async () => {
const result = { test: 'hello' }
global.fetch = jest.fn().mockImplementationOnce(async () => result)
@@ -18,7 +18,7 @@ describe('proxyFetch', () => {
expect(global.fetch.mock.calls.length).toBe(1)
expect(global.fetch.mock.calls[0]).toEqual([
'http://localhost/hoodie/andromeda-viewer/proxy/https/example.com/test',
'http://localhost/api/proxy/https/example.com/test',
{
headers: {
'x-andromeda-session-id': 'test'
@@ -47,7 +47,7 @@ describe('proxyFetch', () => {
expect(global.fetch.mock.calls.length).toBe(1)
expect(global.fetch.mock.calls[0]).toEqual([
'http://localhost/hoodie/andromeda-viewer/proxy/https/example.com/test',
'http://localhost/api/proxy/https/example.com/test',
{
method: 'POST',
headers: new window.Headers([
@@ -146,7 +146,7 @@ describe('fetchLLSD', () => {
})
expect(global.fetch.mock.calls[0]).toEqual([
'http://localhost/hoodie/andromeda-viewer/proxy/https/example.com/test',
'http://localhost/api/proxy/https/example.com/test',
{
headers: {
'x-andromeda-session-id': 'test'
@@ -204,7 +204,7 @@ describe('fetchLLSD', () => {
})
expect(global.fetch.mock.calls[0]).toEqual([
'http://localhost/hoodie/andromeda-viewer/proxy/https/example.com/test',
'http://localhost/api/proxy/https/example.com/test',
{
method: 'POST',
headers: new window.Headers([