3box
Version:
Interact with user data
549 lines (466 loc) • 22.3 kB
JavaScript
jest.mock('3id-resolver', () => {
const { didResolverMock } = require('../__mocks__/3ID')
return {
getResolver: () => ({'3': didResolverMock})
}
})
const utils = require('./testUtils')
const ThreadImport = require('../thread')
const OrbitDB = require('orbit-db')
const {
OdbIdentityProvider,
LegacyIPFS3BoxAccessController,
ThreadAccessController,
ModeratorAccessController
} = require('3box-orbitdb-plugins')
const { Resolver } = require('did-resolver')
const Identities = require('orbit-db-identity-provider')
Identities.addIdentityProvider(OdbIdentityProvider)
const AccessControllers = require('orbit-db-access-controllers')
AccessControllers.addAccessController({ AccessController: LegacyIPFS3BoxAccessController })
AccessControllers.addAccessController({ AccessController: ThreadAccessController })
AccessControllers.addAccessController({ AccessController: ModeratorAccessController })
const { threeIDMockFactory, mockDidResolver } = require('../__mocks__/3ID')
const naclUtil = require('tweetnacl-util')
const nacl = {}
nacl.util = require('tweetnacl-util')
const DID1 = 'did:3:zdpuAsaK9YsqpphSBeQvfrKAjs8kF7vUX4Y3kMkMRgEQigzCt'
const DID2 = 'did:3:zdpuB2DcKQKNBDz3difEYxjTupsho5VuPCLgRbRunXqhmrJaX'
const DID3 = 'did:3:zdpuAvKY6Noex9nRMp1pR8dHauT5Rn4yhVuLq9bAdz4pp5oRd'
const THREAD1_NAME = '3box.thread.somespace.name1'
const THREAD2_NAME = '3box.thread.somespace.name2'
const THREAD3_NAME = '3box.thread.somespace.name2'
const randomThreadName = () => `3box.thread.somespace.name${Math.floor(Math.random() * 1000)}`
const MSG1 = 'message1'
const MSG2 = 'message2'
const MSG3 = 'message3'
const MSG4 = 'message4'
const MSGObj = { "greetings": "hello" }
const THREEID1_MOCK = threeIDMockFactory(DID1)
const THREEID2_MOCK = threeIDMockFactory(DID2)
const THREEID3_MOCK = threeIDMockFactory(DID3)
const subscribeMock = jest.fn()
const userMock = (did) => {
return {
encrypt: (msg) => {
const obj = {
nonce: '123',
ciphertext: Buffer.from(msg).toString('base64')
}
return Promise.resolve(obj)
},
decrypt: (obj) => {
return Promise.resolve( Buffer.from(obj.ciphertext, 'base64'))
},
DID: did
}
}
const Thread = (threadName, replicator, members, firstMod, conf, user, sub) => {
return new ThreadImport(threadName, replicator, members, firstMod, conf || true, userMock(user), sub)
}
describe('Confidential Thread', () => {
let ipfs
let orbitdb
let thread
let storeAddr
let replicatorMock
jest.setTimeout(20000)
beforeAll(async () => {
ipfs = await utils.initIPFS(24)
OdbIdentityProvider.setDidResolver(mockDidResolver)
const identity = await Identities.createIdentity({ id: 'nullid', identityKeysPath: './tmp/odbIdentityKeys-thread' })
orbitdb = await OrbitDB.createInstance(ipfs, {
directory:'./tmp/orbitdb4',
identity
})
replicatorMock = {
_orbitdb: orbitdb,
ensureConnected: jest.fn()
}
})
beforeEach(() => {
subscribeMock.mockClear()
replicatorMock.ensureConnected.mockClear()
})
it('creates thread correctly', async () => {
thread = new Thread(THREAD1_NAME, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
})
it('should throw if not loaded', async () => {
await expect(thread.post('key')).rejects.toThrow(/_load must/)
await expect(thread.getPosts()).rejects.toThrow(/_load must/)
await expect(thread.onUpdate(() => {})).rejects.toThrow(/_load must/)
})
it('should throw if not authed', async () => {
storeAddr = await thread._load()
await expect(thread.post('key')).rejects.toThrow(/You must/)
await expect(thread.addModerator('key')).rejects.toThrow(/You must/)
await expect(thread.addMember('key')).rejects.toThrow(/You must/)
await expect(thread.deletePost('key')).rejects.toThrow(/You must/)
// load should call this:
expect(replicatorMock.ensureConnected).toHaveBeenCalledTimes(1)
expect(replicatorMock.ensureConnected).toHaveBeenCalledWith(storeAddr, true)
})
it('should start with an empty db on load', async () => {
await thread._setIdentity(await THREEID1_MOCK.getOdbId())
expect(storeAddr.split('/')[3]).toEqual(THREAD1_NAME)
expect(await thread.getPosts()).toEqual([])
})
it('adding posts works as expected', async () => {
await thread.post(MSG1)
let posts = await thread.getPosts()
expect(posts[0].author).toEqual(THREEID1_MOCK.DID)
expect(posts[0].message).toEqual(MSG1)
expect(subscribeMock).toHaveBeenCalledTimes(1)
expect(replicatorMock.ensureConnected).toHaveBeenCalledTimes(1)
expect(replicatorMock.ensureConnected).toHaveBeenNthCalledWith(1, storeAddr, true)
await thread.post(MSG2)
posts = await thread.getPosts()
expect(posts[0].message).toEqual(MSG1)
expect(posts[1].message).toEqual(MSG2)
expect(subscribeMock).toHaveBeenCalledTimes(2)
expect(replicatorMock.ensureConnected).toHaveBeenCalledTimes(2)
expect(replicatorMock.ensureConnected).toHaveBeenNthCalledWith(2, storeAddr, true)
await thread.post(MSG3)
posts = await thread.getPosts()
expect(posts[0].message).toEqual(MSG1)
expect(posts[1].message).toEqual(MSG2)
expect(posts[2].message).toEqual(MSG3)
expect(subscribeMock).toHaveBeenCalledTimes(3)
expect(replicatorMock.ensureConnected).toHaveBeenCalledTimes(3)
expect(replicatorMock.ensureConnected).toHaveBeenNthCalledWith(3, storeAddr, true)
})
it('adding object posts works as expected', async () => {
const threadName = randomThreadName()
const thread = new Thread(threadName, replicatorMock, true, DID1, undefined, DID1, subscribeMock)
await thread._load()
await thread._setIdentity(await THREEID1_MOCK.getOdbId())
await thread.post(MSGObj)
let posts = await thread.getPosts()
expect(posts[0].author).toEqual(THREEID1_MOCK.DID)
expect(posts[0].message).toEqual(MSGObj)
})
it('root moderator can add another moderator', async () => {
const threadName = randomThreadName()
const threadMod1 = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod1._load()
await threadMod1._setIdentity(await THREEID1_MOCK.getOdbId())
expect(await threadMod1.listModerators()).toEqual([DID1])
await threadMod1.addModerator(DID2)
expect(await threadMod1.listModerators()).toEqual([DID1, DID2])
})
it('a moderator can add another moderator', async () => {
const threadName = randomThreadName()
const threadMod1 = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod1._load()
await threadMod1._setIdentity(await THREEID1_MOCK.getOdbId())
expect(await threadMod1.listModerators()).toEqual([DID1])
await threadMod1.addModerator(DID2)
expect(await threadMod1.listModerators()).toEqual([DID1, DID2])
const threadMod2 = new Thread(threadName, replicatorMock, false, DID1, threadMod1._encKeyId, DID2, subscribeMock)
await threadMod2._load()
await threadMod2._setIdentity(await THREEID2_MOCK.getOdbId())
await threadMod2.addModerator(DID3)
expect(await threadMod2.listModerators()).toEqual([DID1, DID2, DID3])
})
it('moderator can add a Member', async () => {
const threadName = randomThreadName()
const threadMembers = new Thread(threadName, replicatorMock, true, DID1, undefined, DID1, subscribeMock)
await threadMembers._load()
await threadMembers._setIdentity(await THREEID1_MOCK.getOdbId())
expect(await threadMembers.listMembers()).toEqual([])
await threadMembers.addMember(DID2)
expect(await threadMembers.listMembers()).toEqual([DID2])
})
it('user who is not a moderator can NOT add a Member', async () => {
const threadName = randomThreadName()
// Mod creates thread
const threadMod1 = new Thread(threadName, replicatorMock, true, DID1, undefined, DID1, subscribeMock)
await threadMod1._load()
await threadMod1._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod1.addMember(DID2)
// member opens and tries to add member
const threadMembers = new Thread(threadName, replicatorMock, true, DID1, threadMod1._encKeyId, DID2, subscribeMock)
await threadMembers._load()
await threadMembers._setIdentity(await THREEID2_MOCK.getOdbId())
await expect(threadMembers.addMember(DID3)).rejects.toThrow(/can not be granted/)
expect(await threadMembers.listMembers()).toEqual([DID2])
})
it('a moderator can delete other users posts', async () => {
const threadName = randomThreadName()
// create thread by mod, with members
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
await threadMod.addModerator(DID3)
// user 1, member adds post
const threadUser1 = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadUser1._load()
await threadUser1._setIdentity(await THREEID2_MOCK.getOdbId())
await threadUser1.post(MSG1)
const posts = await threadUser1.getPosts()
const entryId = posts[0].postId
// user 2, a mod, deletes post by user 1
const threadMod2 = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMod2._load()
await threadMod2._setIdentity(await THREEID3_MOCK.getOdbId())
await threadMod2.deletePost(entryId)
expect(await threadMod2.getPosts()).toEqual([])
})
it('user without being a moderator can NOT delete others posts', async () => {
const threadName = randomThreadName()
// create thread by mod, with members
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
await threadMod.addMember(DID3)
// user 1, member adds post
const threadUser1 = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadUser1._load()
await threadUser1._setIdentity(await THREEID2_MOCK.getOdbId())
await threadUser1.post(MSG1)
const posts = await threadUser1.getPosts()
const entryId = posts[0].postId
// user 2, member tries to delete post
const threadUser2 = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadUser2._load()
await threadUser2._setIdentity(await THREEID3_MOCK.getOdbId())
await expect(threadUser2.deletePost(entryId)).rejects.toThrow(/not append entry/)
})
it('user without being a moderator can delete their own posts', async () => {
const threadName = randomThreadName()
// create thread by mod, with member
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
// member write post
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await threadMember._setIdentity(await THREEID2_MOCK.getOdbId())
await threadMember.post(MSG1)
//Member delete their own post
const posts = await threadMember.getPosts()
const entryId = posts[0].postId
await threadMember.deletePost(entryId)
expect(await threadMember.getPosts()).toEqual([])
})
it('non member can NOT join a confidential thread', async () => {
const threadName = randomThreadName()
// create thread by mod, no members
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
// Non member try to post
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await expect(threadMember._setIdentity(await THREEID2_MOCK.getOdbId())).rejects.toThrow(/no access/)
})
it('a member can post in a confidential thread', async () => {
const threadName = randomThreadName()
// create thread by mod, with member
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
// member write post
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await threadMember._setIdentity(await THREEID2_MOCK.getOdbId())
await threadMember.post(MSG1)
const posts = await threadMember.getPosts()
await expect(posts[0].message).toEqual(MSG1)
})
it('a member can NOT add a member', async () => {
const threadName = randomThreadName()
// create thread by mod, with member
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
// member tries to add member
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await threadMember._setIdentity(await THREEID2_MOCK.getOdbId())
await expect(threadMember.addMember(DID3)).rejects.toThrow(/can not be granted/)
expect(await threadMember.listMembers()).toEqual([DID2])
})
it('a member can NOT add a moderator', async () => {
const threadName = randomThreadName()
// create thread by mod, with member
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
// member tries to add member
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await threadMember._setIdentity(await THREEID2_MOCK.getOdbId())
await expect(threadMember.addModerator(DID3)).rejects.toThrow(/can not be granted/)
expect(await threadMember.listModerators()).toEqual([DID1])
})
it('a member upgraded to a moderator can add other mods', async () => {
const threadName = randomThreadName()
// create thread by mod, with member
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
await threadMod.addModerator(DID2)
//now mod, adds mod
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await threadMember._setIdentity(await THREEID2_MOCK.getOdbId())
await threadMember.addModerator(DID3)
expect(await threadMember.listModerators()).toEqual([DID1, DID2, DID3])
})
it('a member upgraded to a moderator can add other members', async () => {
const threadName = randomThreadName()
// create thread by mod, with member
const threadMod = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadMod._load()
await threadMod._setIdentity(await THREEID1_MOCK.getOdbId())
await threadMod.addMember(DID2)
await threadMod.addModerator(DID2)
//now mod, adds members
const threadMember = new Thread(threadName, replicatorMock, false, DID1, threadMod._encKeyId, DID2, subscribeMock)
await threadMember._load()
await threadMember._setIdentity(await THREEID2_MOCK.getOdbId())
await threadMember.addMember(DID3)
expect(await threadMember.listMembers()).toEqual([DID2, DID3])
})
it('a thread can be loaded by its address only', async () => {
const threadName = randomThreadName()
const thread1 = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await thread1._load()
await thread1._setIdentity(await THREEID1_MOCK.getOdbId())
await thread1._initConfidential()
await thread1.addMember(DID2)
const thread1Address = thread1.address
await thread1.post(MSG1)
const thread2 = new ThreadImport(undefined, replicatorMock, undefined, undefined, undefined, userMock(DID2), subscribeMock)
await thread2._load(thread1Address)
await thread2._setIdentity(await THREEID2_MOCK.getOdbId())
const thread2Address = thread2.address
expect(thread2Address).toEqual(thread1Address)
const posts = await thread2.getPosts()
await expect(posts[0].message).toEqual(MSG1)
})
describe('multi user interaction', () => {
let threadUser1
let threadUser2
let ipfs2
let orbitdb2
let replicatorMock2
beforeAll(async () => {
ipfs2 = await utils.initIPFS(25)
orbitdb2 = await OrbitDB.createInstance(ipfs2, {
directory:'./tmp/orbitdb5'
})
replicatorMock2 = {
_orbitdb: orbitdb2,
ensureConnected: jest.fn()
}
})
it('syncs thread between users', async () => {
threadUser1 = new Thread(THREAD2_NAME, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadUser1._load()
await threadUser1._setIdentity(await THREEID1_MOCK.getOdbId())
await threadUser1.addMember(DID2)
threadUser2 = new Thread(THREAD2_NAME, replicatorMock2, false, DID1, threadUser1._encKeyId, DID2, subscribeMock)
await threadUser2._load()
await threadUser2._setIdentity(await THREEID2_MOCK.getOdbId())
// user1 posts and user2 receives
// done needed to not catch the write event
let done = false
let postPromise = new Promise((resolve, reject) => {
threadUser2.onUpdate(post => {
if (!done) {
done = true
}
resolve()
})
})
await threadUser1.post(MSG1)
let posts1 = await threadUser1.getPosts()
expect(posts1[0].author).toEqual(THREEID1_MOCK.DID)
expect(posts1[0].message).toEqual(MSG1)
await postPromise
threadUser2.onUpdate(() => {})
await new Promise((resolve, reject) => { setTimeout(resolve, 500) })
let posts2 = await threadUser2.getPosts()
expect(posts2[0].author).toEqual(THREEID1_MOCK.DID)
expect(posts2[0].message).toEqual(MSG1)
expect(posts2[0].postId).toEqual(posts1[0].postId)
// user2 posts and user1 receives
postPromise = new Promise((resolve, reject) => {
threadUser1.onUpdate(() => {
resolve()
})
})
await threadUser2.post(MSG2)
posts2 = await threadUser2.getPosts()
expect(posts2[1].author).toEqual(THREEID2_MOCK.DID)
expect(posts2[1].message).toEqual(MSG2)
await postPromise
await new Promise((resolve, reject) => { setTimeout(resolve, 500) })
posts1 = await threadUser1.getPosts()
expect(posts1[1].author).toEqual(THREEID2_MOCK.DID)
expect(posts1[1].message).toEqual(MSG2)
expect(posts1[1].postId).toEqual(posts2[1].postId)
})
it('moderator capabilities are shared/synced between users', async () => {
const threadName = randomThreadName()
threadUser1 = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadUser1._load()
await threadUser1._setIdentity(await THREEID1_MOCK.getOdbId())
await threadUser1.addMember(DID2)
threadUser2 = new Thread(threadName, replicatorMock2, false, DID1, threadUser1._encKeyId, DID2, subscribeMock)
await threadUser2._load()
await threadUser2._setIdentity(await THREEID2_MOCK.getOdbId())
// user two tries to add a moderator and fails
await expect(threadUser2.addModerator(DID3)).rejects.toThrow(/can not be granted/)
expect(await threadUser2.listModerators()).toEqual([DID1])
expect(await threadUser1.listModerators()).toEqual([DID1])
// user1 add user2 as modes, wait for AC update
let updatePromise = new Promise((resolve, reject) => {
threadUser2.onNewCapabilities(resolve)
})
// user one adds user 2 as mod
await threadUser1.addModerator(DID2)
await updatePromise
expect(await threadUser2.listModerators()).toEqual([DID1, DID2])
expect(await threadUser1.listModerators()).toEqual([DID1, DID2])
// user 2 adds another mod succsesfully now
await threadUser2.addModerator(DID3)
expect(await threadUser2.listModerators()).toEqual([DID1, DID2, DID3])
})
it('member capabilities are shared/synced between users', async () => {
const threadName = randomThreadName()
threadUser1 = new Thread(threadName, replicatorMock, false, DID1, undefined, DID1, subscribeMock)
await threadUser1._load()
await threadUser1._setIdentity(await THREEID1_MOCK.getOdbId())
threadUser2 = new Thread(threadName, replicatorMock2, false, DID1, threadUser1._encKeyId, DID2, subscribeMock)
await threadUser2._load()
await expect(threadUser2._setIdentity(await THREEID2_MOCK.getOdbId())).rejects.toThrow(/no access/)
// user one adds user 2 as member
await threadUser1.addMember(DID2)
await threadUser2._setIdentity(await THREEID2_MOCK.getOdbId())
expect(await threadUser2.listMembers()).toEqual([DID2])
expect(await threadUser1.listMembers()).toEqual([DID2])
// user 2 adds a post now
await threadUser2.post(MSG1)
const posts = await threadUser2.getPosts()
await expect(posts[0].message).toEqual(MSG1)
})
afterAll(async () => {
await orbitdb2.stop()
return utils.stopIPFS(ipfs2, 25)
})
})
afterAll(async () => {
await orbitdb.stop()
return utils.stopIPFS(ipfs, 24)
})
})