diff --git a/src/database/Persona/Persona.db.ts b/src/database/Persona/Persona.db.ts index 0cef0cf84708..a9452fac3f57 100644 --- a/src/database/Persona/Persona.db.ts +++ b/src/database/Persona/Persona.db.ts @@ -37,6 +37,7 @@ const db = createDBAccess(() => { }, }) }) +export const createPersonaDBAccess = db export type FullPersonaDBTransaction = IDBPSafeTransaction< PersonaDB, ['personas', 'profiles'], @@ -186,7 +187,7 @@ export async function updatePersonaDB( ...old, ...nextRecord, linkedProfiles: nextLinkedProfiles, - updatedAt: new Date(), + updatedAt: nextRecord.updatedAt ?? new Date(), }) await t.objectStore('personas').put(next) MessageCenter.emit('personaUpdated', undefined) @@ -202,8 +203,8 @@ export async function createOrUpdatePersonaDB( return createPersonaDB( { ...record, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: record.createdAt ?? new Date(), + updatedAt: record.updatedAt ?? new Date(), linkedProfiles: new IdentifierMap(new Map()), }, t, diff --git a/src/database/Persona/consistency.ts b/src/database/Persona/consistency.ts index be330207babf..12e675d82451 100644 --- a/src/database/Persona/consistency.ts +++ b/src/database/Persona/consistency.ts @@ -121,7 +121,7 @@ async function* checkProfileLink( const designatedPersona = restorePrototype(invalidLinkedPersona, ECKeyIdentifier.prototype) const persona = await t.objectStore('personas').get(designatedPersona.toText()) if (!persona) { - yield { type: Type.One_Way_Link_In_Profile, profile: profile, designatedPersona } + yield { type: Type.One_Way_Link_In_Profile, profile, designatedPersona } } } diff --git a/src/database/Persona/helpers.ts b/src/database/Persona/helpers.ts index 58ef4ca525d6..a2321dc1d803 100644 --- a/src/database/Persona/helpers.ts +++ b/src/database/Persona/helpers.ts @@ -95,7 +95,7 @@ export async function queryProfilesWithQuery(query?: Parameters[0]): Promise { const _ = await queryPersonasDB(query || (_ => true)) @@ -193,6 +193,7 @@ export async function createProfileWithPersona( profileID: ProfileIdentifier, data: LinkedProfileDetails, keys: { + nickname?: string publicKey: JsonWebKey privateKey?: JsonWebKey localKey?: CryptoKey @@ -205,6 +206,7 @@ export async function createProfileWithPersona( updatedAt: new Date(), identifier: ec_id, linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), + nickname: keys.nickname, publicKey: keys.publicKey, privateKey: keys.privateKey, localKey: keys.localKey, diff --git a/src/database/__tests__/Persona/Persona.db.ts b/src/database/__tests__/Persona/Persona.db.ts new file mode 100644 index 000000000000..1bf14c55ebd0 --- /dev/null +++ b/src/database/__tests__/Persona/Persona.db.ts @@ -0,0 +1,280 @@ +import uuid from 'uuid/v4' +import { IdentifierMap } from '../../IdentifierMap' +import { ProfileIdentifier, ECKeyIdentifier } from '../../type' +import { + PersonaRecord, + ProfileRecord, + createPersonaDB, + queryPersonaDB, + FullPersonaDBTransaction, + createPersonaDBAccess, + queryPersonasDB, + deletePersonaDB, + updatePersonaDB, + LinkedProfileDetails, + createProfileDB, + queryProfileDB, + queryProfilesDB, + updateProfileDB, + createOrUpdateProfileDB, + deleteProfileDB, + attachProfileDB, + queryPersonaByProfileDB, + queryPersonasWithPrivateKey, + createOrUpdatePersonaDB, + safeDeletePersonaDB, +} from '../../Persona/Persona.db' +import { generate_ECDH_256k1_KeyPair_ByMnemonicWord } from '../../../utils/mnemonic-code' +import { CryptoKeyToJsonWebKey } from '../../../utils/type-transform/CryptoKey-JsonWebKey' +import { deriveLocalKeyFromECDHKey } from '../../../utils/mnemonic-code/localKeyGenerate' +import { createTransaction } from '../../helpers/openDB' + +export async function createPersonaRecord(name: string = uuid(), password: string = uuid()) { + const key = await generate_ECDH_256k1_KeyPair_ByMnemonicWord(password) + const jwkPub = await CryptoKeyToJsonWebKey(key.key.publicKey) + const jwkPriv = await CryptoKeyToJsonWebKey(key.key.privateKey) + const localKey = await deriveLocalKeyFromECDHKey(key.key.publicKey, key.mnemonicRecord.words) + const jwkLocalKey = await CryptoKeyToJsonWebKey(localKey) + const identifier = ECKeyIdentifier.fromJsonWebKey(jwkPub) + + return { + nickname: name, + createdAt: new Date(), + updatedAt: new Date(), + identifier: identifier, + linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), + publicKey: jwkPub, + privateKey: jwkPriv, + mnemonic: key.mnemonicRecord, + localKey: jwkLocalKey, + } as PersonaRecord +} + +export async function createProfileRecord( + identifier: ProfileIdentifier = new ProfileIdentifier(uuid(), uuid()), + name: string = uuid(), + password: string = uuid(), +) { + const { localKey, identifier: linkedPersona, createdAt, updatedAt } = await createPersonaRecord(name, password) + + return { + identifier, + nickname: name, + localKey, + linkedPersona, + createdAt, + updatedAt, + } as ProfileRecord +} + +export async function personaDBWriteAccess(action: (t: FullPersonaDBTransaction<'readwrite'>) => Promise) { + await action(createTransaction(await createPersonaDBAccess(), 'readwrite')('profiles', 'personas')) +} + +beforeAll(() => { + // MessageCenter will dispatch events on each tab + // but the mocking query method returns undefined + browser.tabs.query = async () => { + return [] + } +}) + +afterEach(async () => { + await personaDBWriteAccess(async t => { + await t.objectStore('personas').clear() + await t.objectStore('profiles').clear() + }) +}) + +test('createPersonaDB & queryPersonaDB', async () => { + const personaRecord = await createPersonaRecord() + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) +}) + +test('queryPersonasDB', async () => { + const personaRecordA = await createPersonaRecord() + const personaRecordB = await createPersonaRecord() + const names = [personaRecordA.nickname, personaRecordB.nickname] + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecordA, t) + await createPersonaDB(personaRecordB, t) + }) + + const personaRecords = await queryPersonasDB(p => !!(p.nickname && names.includes(p.nickname))) + expect(personaRecords.length).toBe(2) + expect(personaRecords.every(r => names.includes(r.nickname ?? ''))).toBeTruthy() +}) + +test('updatePersonaDB - replace linked profiles', async () => { + const id = uuid() + const personaRecord = await createPersonaRecord(id, id) + const personaRecordNew = await createPersonaRecord(id, id) + personaRecordNew.identifier = personaRecord.identifier + + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecord, t) + await updatePersonaDB( + personaRecordNew, + { + linkedProfiles: 'replace', + explicitUndefinedField: 'ignore', + }, + t, + ) + }) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecordNew) +}) + +test('deletePersonaDB', async () => { + const personaRecord = await createPersonaRecord() + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + await personaDBWriteAccess(t => deletePersonaDB(personaRecord.identifier, 'delete even with private', t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) +}) + +test('queryPersonaByProfileDB', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + + expect(await queryPersonaByProfileDB(profileRecord.identifier)).toEqual(personaRecord) +}) + +test('queryPersonasWithPrivateKey', async () => { + const personaRecordA = await createPersonaRecord() + const personaRecordB = await createPersonaRecord() + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecordA, t) + await createPersonaDB(personaRecordB, t) + }) + const names = (await queryPersonasWithPrivateKey()).map(p => p.nickname) + + expect(names.includes(personaRecordA.nickname)).toBe(true) + expect(names.includes(personaRecordB.nickname)).toBe(true) +}) + +test('createOrUpdatePersonaDB', async () => { + const personaRecord = await createPersonaRecord() + const personaRecordNew = await createPersonaRecord() + const howToMerge = { + linkedProfiles: 'replace', + explicitUndefinedField: 'ignore', + } as Parameters[1] + personaRecordNew.identifier = personaRecord.identifier + + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) + + await personaDBWriteAccess(t => createOrUpdatePersonaDB(personaRecord, howToMerge, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + await personaDBWriteAccess(t => createOrUpdatePersonaDB(personaRecordNew, howToMerge, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecordNew) +}) + +test('safeDeletePersonaDB', async () => { + const personaRecord = await createPersonaRecord() + const howToMerge = { + linkedProfiles: 'replace', + explicitUndefinedField: 'delete field', + } as Parameters[1] + + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + personaRecord.linkedProfiles.clear() + await personaDBWriteAccess(async t => { + await updatePersonaDB(personaRecord, howToMerge, t) + await safeDeletePersonaDB(personaRecord.identifier, t) + }) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + personaRecord.privateKey = undefined + await personaDBWriteAccess(async t => { + await updatePersonaDB(personaRecord, howToMerge, t) + await safeDeletePersonaDB(personaRecord.identifier, t) + }) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) +}) + +test('createProfileDB && queryProfileDB', async () => { + const profileRecord = await createProfileRecord() + await personaDBWriteAccess(t => createProfileDB(profileRecord, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecord) +}) + +test('queryProfilesDB', async () => { + const profileRecordA = await createProfileRecord() + const profileRecordB = await createProfileRecord() + const nicknames = [profileRecordA.nickname, profileRecordB.nickname] + const networkIds = [profileRecordA.identifier.network, profileRecordB.identifier.network] + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecordA, t) + await createProfileDB(profileRecordB, t) + }) + + const profileRecords = await queryProfilesDB(p => networkIds.includes(p.identifier.network)) + expect(profileRecords.length).toBe(2) + expect(profileRecords.every(r => nicknames.includes(r.nickname ?? ''))).toBeTruthy() +}) + +test('updateProfileDB', async () => { + const profileRecord = await createProfileRecord() + const profileRecordNew = await createProfileRecord() + profileRecordNew.identifier = profileRecord.identifier + + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await updateProfileDB(profileRecordNew, t) + }) + + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecordNew) +}) + +test('createOrUpdateProfileDB', async () => { + const profileRecord = await createProfileRecord() + const profileRecordNew = await createProfileRecord() + profileRecordNew.identifier = profileRecord.identifier + + expect(await queryProfileDB(profileRecord.identifier)).toEqual(null) + + await personaDBWriteAccess(t => createOrUpdateProfileDB(profileRecord, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecord) + + await personaDBWriteAccess(t => createOrUpdateProfileDB(profileRecordNew, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecordNew) +}) + +test('attachProfileDB && detachProfileDB', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecord, t) + await attachProfileDB( + profileRecord.identifier, + personaRecord.identifier, + { + connectionConfirmState: 'confirmed', + }, + t, + ) + }) + expect((await queryProfileDB(profileRecord.identifier))?.linkedPersona).toEqual(personaRecord.identifier) +}) + +test('deleteProfileDB', async () => { + const profileRecord = await createProfileRecord() + + await personaDBWriteAccess(t => createProfileDB(profileRecord, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecord) + + await personaDBWriteAccess(t => deleteProfileDB(profileRecord.identifier, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(null) +}) diff --git a/src/database/__tests__/Persona/helpers.ts b/src/database/__tests__/Persona/helpers.ts new file mode 100644 index 000000000000..ff5aba25f3ae --- /dev/null +++ b/src/database/__tests__/Persona/helpers.ts @@ -0,0 +1,233 @@ +import uuid from 'uuid/v4' +import { web3 } from '../../../plugins/Wallet/web3' +import { + createPersonaByMnemonic, + queryPersona, + profileRecordToProfile, + personaRecordToPersona, + queryProfile, + queryProfilesWithQuery, + queryPersonasWithQuery, + renamePersona, + queryPersonaByProfile, + queryPersonaRecord, + queryPublicKey, + queryLocalKey, + queryPrivateKey, + createProfileWithPersona, + deletePersona, +} from '../../Persona/helpers' +import { queryPersonaDB, createProfileDB, createPersonaDB, queryProfileDB } from '../../Persona/Persona.db' +import { createPersonaRecord, createProfileRecord, personaDBWriteAccess } from './Persona.db' +import { storeAvatarDB } from '../../avatar' +import { JsonWebKeyToCryptoKey, getKeyParameter } from '../../../utils/type-transform/CryptoKey-JsonWebKey' + +beforeAll(() => { + // MessageCenter will dispatch events on each tab + // but default query method returns undefined + browser.tabs.query = async () => { + return [] + } +}) + +afterEach(async () => { + await personaDBWriteAccess(async t => { + await t.objectStore('personas').clear() + await t.objectStore('profiles').clear() + }) +}) + +test('profileRecordToProfile', async () => { + const profileRecord = await createProfileRecord() + await storeAvatarDB(profileRecord.identifier, new ArrayBuffer(20)) + + const profile = await profileRecordToProfile(profileRecord) + expect(profile.avatar).toBe('data:image/png;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAA=') + expect(profile.linkedPersona?.identifier).toEqual(profileRecord.linkedPersona) +}) + +test('personaRecordToPersona', async () => { + const personaRecord = await createPersonaRecord() + const persona = personaRecordToPersona(personaRecord) + + expect(persona.hasPrivateKey).toBe(true) + expect(persona.fingerprint).toBe(personaRecord.identifier.compressedPoint) +}) + +test('queryProfile', async () => { + const profileRecord = await createProfileRecord() + const fake = await queryProfile(profileRecord.identifier) + expect(fake.avatar).toBe(undefined) + expect(fake.linkedPersona).toBe(undefined) + + await storeAvatarDB(profileRecord.identifier, new ArrayBuffer(20)) + await personaDBWriteAccess(t => createProfileDB(profileRecord, t)) + + const real = await queryProfile(profileRecord.identifier) + expect(real.avatar).toBe('data:image/png;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAA=') + expect(real.linkedPersona?.identifier).toEqual(profileRecord.linkedPersona) +}) + +test('queryPersona', async () => { + const personaRecord = await createPersonaRecord() + const fake = await queryPersona(personaRecord.identifier) + expect(fake.hasPrivateKey).toBe(false) + expect(fake.fingerprint).toBe(personaRecord.identifier.compressedPoint) + + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + + const real = await queryPersona(personaRecord.identifier) + expect(real.hasPrivateKey).toBe(true) + expect(real.fingerprint).toBe(personaRecord.identifier.compressedPoint) +}) + +test('queryProfilesWithQuery', async () => { + const profileRecordA = await createProfileRecord() + const profileRecordB = await createProfileRecord() + const names = [profileRecordA.nickname, profileRecordB.nickname] + + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecordA, t) + await createProfileDB(profileRecordB, t) + }) + + const profiles = await queryProfilesWithQuery(({ nickname }) => names.includes(nickname)) + expect(profiles.every(p => names.includes(p.nickname))).toBe(true) +}) + +test('queryPersonasWithQuery', async () => { + const personaRecordA = await createPersonaRecord() + const personaRecordB = await createPersonaRecord() + const names = [personaRecordA.nickname, personaRecordB.nickname] + + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecordA, t) + await createPersonaDB(personaRecordB, t) + }) + + const personas = await queryPersonasWithQuery(({ nickname }) => names.includes(nickname)) + expect(personas.every(p => names.includes(p.nickname))).toBe(true) +}) + +test('deletePersona', async () => { + const personaRecord = await createPersonaRecord() + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + await deletePersona(personaRecord.identifier, 'delete even with private') + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) +}) + +test('renamePersona', async () => { + const name = uuid() + const personaRecord = await createPersonaRecord() + + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + await renamePersona(personaRecord.identifier, name) + expect((await queryPersonaDB(personaRecord.identifier))?.nickname).toBe(name) +}) + +test('queryPersonaByProfile', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPersonaByProfile(profileRecord.identifier)).toEqual(personaRecordToPersona(personaRecord)) +}) + +test('queryPersonaRecord', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPersonaRecord(profileRecord.identifier)).toEqual(personaRecord) + expect(await queryPersonaRecord(personaRecord.identifier)).toEqual(personaRecord) +}) + +test('queryPublicKey', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + const publicKey = await JsonWebKeyToCryptoKey(personaRecord.publicKey, ...getKeyParameter('ecdh')) + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPublicKey(profileRecord.identifier)).toEqual(publicKey) + expect(await queryPublicKey(personaRecord.identifier)).toEqual(publicKey) +}) + +test('queryPrivateKey', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + const privateKey = await JsonWebKeyToCryptoKey(personaRecord.privateKey!, ...getKeyParameter('ecdh')) + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPrivateKey(profileRecord.identifier)).toEqual(privateKey) + expect(await queryPrivateKey(personaRecord.identifier)).toEqual(privateKey) +}) + +test('createPersonaByMnemonic & createPersonaByJsonWebKey', async () => { + // getBalance will be called in the event chain reaction + web3.eth.getBalance = async () => '0' + + const identifier = await createPersonaByMnemonic('test', 'test') + const persona = await queryPersonaDB(identifier) + expect(persona?.identifier).toEqual(identifier) + expect(persona?.nickname).toEqual('test') + expect(persona?.mnemonic?.parameter.withPassword).toEqual(true) +}) + +test('createProfileWithPersona', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + await createProfileWithPersona( + profileRecord.identifier, + { + connectionConfirmState: 'confirmed', + }, + personaRecord, + ) + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + expect((await queryProfileDB(profileRecord.identifier))?.linkedPersona).toEqual(personaRecord.identifier) + expect((await queryPersonaDB(personaRecord.identifier))?.identifier).toEqual(personaRecord.identifier) +}) + +test('queryLocalKey', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryLocalKey(profileRecord.identifier)).toEqual(profileRecord.localKey) + expect(await queryLocalKey(personaRecord.identifier)).toEqual(personaRecord.localKey) +})