From 38acb81bea559598131dd9991c973af5a9b4d936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rib=C3=B3?= Date: Sun, 1 Oct 2023 21:19:02 +0200 Subject: [PATCH] feat: Implement Messaging and improve test coverage to decent levels. --- package.json | 8 +- src/index.ts | 170 +++++++++++++++++++++++++++++++---------- src/schemas/Message.ts | 52 ++++++++++++- tests/pluto.test.ts | 86 ++++++++++++++++++++- 4 files changed, 267 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index f31e241d..a5e391fc 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,10 @@ ], "coverageThreshold": { "global": { - "branches": 36, - "functions": 61, - "lines": 58, - "statements": 59 + "branches": 58, + "functions": 71, + "lines": 74, + "statements": 71 } }, "coverageDirectory": "coverage" diff --git a/src/index.ts b/src/index.ts index 0f6d5d5c..99c6e95a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { } from "@input-output-hk/atala-prism-wallet-sdk"; import { getRxStorageDexie } from "rxdb/plugins/storage-dexie"; import { wrappedKeyEncryptionCryptoJsStorage } from "rxdb/plugins/encryption-crypto-js"; -import { RxCollection, RxDatabase, RxDatabaseCreator, RxDocument, RxJsonSchema, createRxDatabase } from "rxdb"; +import { MangoQuerySelector, RxCollection, RxDatabase, RxDatabaseCreator, RxDocument, RxJsonSchema, createRxDatabase } from "rxdb"; import { RxError } from "rxdb/dist/lib/rx-error"; import { addRxPlugin } from "rxdb"; import { RxDBMigrationPlugin } from "rxdb/plugins/migration"; @@ -93,11 +93,19 @@ export class Database implements Domain.Pluto { } async getMessage(id: string): Promise { - return this.db.messages.findOne().where({ id: id }).exec(); + const message = await this.db.messages.findOne().where({ id: id }).exec() + if (message) { + return this.getDomainMessage(message) + } + return null; } async storeMessage(message: Domain.Message): Promise { - await this.db.messages.insert(message); + await this.db.messages.insert({ + ...message, + to: message.to?.toString(), + from: message.from?.toString() + }); } async storeMessages(messages: Domain.Message[]): Promise { @@ -105,7 +113,8 @@ export class Database implements Domain.Pluto { } async getAllMessages(): Promise { - return this.db.messages.find().exec(); + const messages = await this.db.messages.find().exec() + return messages.map((message) => this.getDomainMessage(message)) } async start(): Promise { @@ -198,21 +207,21 @@ export class Database implements Domain.Pluto { privateKey: Domain.PrivateKey, did: Domain.DID, keyPathIndex: number, - metaId: string | null + metaId?: string | null ): Promise { await this.db.privateKeys.insert({ id: uuidv4(), did: did.toString(), type: privateKey.type, keySpecification: Array.from(privateKey.keySpecification).reduce( - (all, [key, value]) => { - all.push({ + (all, [key, value]) => [ + ...all, + { type: "string", name: key, value: `${value}`, - }); - return all; - }, + }, + ], [ { type: "string", @@ -447,15 +456,18 @@ export class Database implements Domain.Pluto { Domain.DID.fromString(didDB.did) ); - if (privateKey) { - const indexProp = privateKey.getProperty(Domain.KeyProperties.index); - const index = indexProp ? parseInt(indexProp) : undefined; - return new Domain.PrismDIDInfo( - Domain.DID.fromString(didDB.did), - index, - didDB.alias - ); + if (!privateKey) { + throw new Error("Imposible to recover PrismDIDInfo without its privateKey data.") } + + const indexProp = privateKey.getProperty(Domain.KeyProperties.index); + const index = indexProp ? parseInt(indexProp) : undefined; + return new Domain.PrismDIDInfo( + Domain.DID.fromString(didDB.did), + index, + didDB.alias + ); + } return null; @@ -483,53 +495,127 @@ export class Database implements Domain.Pluto { return prismDIDInfo; } - getPrismDIDKeyPathIndex(did: Domain.DID): Promise { - throw new Error("Method not implemented."); + + private getDomainMessage(message: RxDocument) { + return Domain.Message.fromJson(JSON.stringify(message.toJSON())) } - getPrismLastKeyPathIndex(): Promise { - throw new Error("Method not implemented."); + async getAllMessagesByDID(did: Domain.DID): Promise { + const messages = await this.db.messages.find().where({ + $or: [ + { + to: did.toString() + }, + { + from: did.toString() + } + ] + }).exec() + return messages.map(message => this.getDomainMessage(message)) } - storeCredential(credential: Domain.VerifiableCredential): Promise { - throw new Error("Method not implemented."); + async getAllMessagesSent(): Promise { + const messages = await this.db.messages.find().where({ + $or: [ + { + direction: Domain.MessageDirection.SENT + } + ] + }).exec() + return messages.map(message => this.getDomainMessage(message)) } - getAllPeerDIDs(): Promise { - throw new Error("Method not implemented."); + async getAllMessagesReceived(): Promise { + const messages = await this.db.messages.find().where({ + $or: [ + { + direction: Domain.MessageDirection.SENT + } + ] + }).exec() + return messages.map(message => this.getDomainMessage(message)) } - getAllMessagesByDID(did: Domain.DID): Promise { - throw new Error("Method not implemented."); + async getAllMessagesSentTo(did: Domain.DID): Promise { + const messages = await this.db.messages.find().where({ + $or: [ + { + to: did.toString() + } + ] + }).exec() + return messages.map(message => this.getDomainMessage(message)) } - getAllMessagesSent(): Promise { - throw new Error("Method not implemented."); + async getAllMessagesReceivedFrom(did: Domain.DID): Promise { + const messages = await this.db.messages.find().where({ + $or: [ + { + from: did.toString() + } + ] + }).exec() + return messages.map(message => this.getDomainMessage(message)) } - getAllMessagesReceived(): Promise { - throw new Error("Method not implemented."); + async getAllMessagesOfType( + type: string, + relatedWithDID?: Domain.DID | undefined + ): Promise { + const query: MangoQuerySelector[] = [ + { + piuri: type + }, + ]; + if (relatedWithDID) { + query.push({ + $or: [ + { + from: relatedWithDID.toString() + }, + { + to: relatedWithDID.toString() + } + ] + }) + } + const messages = await this.db.messages.find().where({ + $and: query + }).exec() + return messages.map(message => this.getDomainMessage(message)) } - getAllMessagesSentTo(did: Domain.DID): Promise { + async getAllMessagesByFromToDID( + from: Domain.DID, + to: Domain.DID + ): Promise { + const messages = await this.db.messages.find().where({ + $or: [ + { + from: from.toString() + }, + { + to: to.toString() + } + ] + }).exec() + return messages.map(message => this.getDomainMessage(message)) + } + + + getPrismDIDKeyPathIndex(did: Domain.DID): Promise { throw new Error("Method not implemented."); } - getAllMessagesReceivedFrom(did: Domain.DID): Promise { + getPrismLastKeyPathIndex(): Promise { throw new Error("Method not implemented."); } - getAllMessagesOfType( - type: string, - relatedWithDID?: Domain.DID | undefined - ): Promise { + storeCredential(credential: Domain.VerifiableCredential): Promise { throw new Error("Method not implemented."); } - getAllMessagesByFromToDID( - from: Domain.DID, - to: Domain.DID - ): Promise { + getAllPeerDIDs(): Promise { throw new Error("Method not implemented."); } diff --git a/src/schemas/Message.ts b/src/schemas/Message.ts index ab97b276..728eb368 100644 --- a/src/schemas/Message.ts +++ b/src/schemas/Message.ts @@ -1,7 +1,24 @@ import type { Domain } from "@input-output-hk/atala-prism-wallet-sdk"; import type { Schema } from "../types"; -export type MessageSchemaType = Domain.Message; + + +export type MessageSchemaType = { + readonly body: string; + readonly id: string; + readonly piuri: string; + readonly from?: string | undefined; + readonly to?: string | undefined; + readonly attachments: Domain.AttachmentDescriptor[]; + readonly thid?: string; + readonly extraHeaders: string[]; + readonly createdTime: string; + readonly expiresTimePlus: string; + readonly ack: string[]; + readonly direction: Domain.MessageDirection; + readonly fromPrior?: string | undefined; + readonly pthid?: string | undefined; +}; /** * MessageSchema @@ -23,6 +40,39 @@ const MessageSchema: Schema = { }, attachments: { type: "array", + items: { + type: "object", + properties: { + id: { + type: "id", + maxLength: 60, + }, + description: { + type: "string", + }, + byteCount: { + type: "number", + }, + lastModTime: { + type: "string", + }, + format: { + type: 'string' + }, + filename: { + type: "array", + items: { + type: "string" + } + }, + mediaType: { + type: 'string' + }, + data: { + type: 'object' + } + }, + }, }, extraHeaders: { type: "array", diff --git a/tests/pluto.test.ts b/tests/pluto.test.ts index faed0353..c9432cc7 100644 --- a/tests/pluto.test.ts +++ b/tests/pluto.test.ts @@ -9,7 +9,8 @@ import * as Fixtures from "./fixtures"; const databaseName = "prism-db"; const keyData = new Uint8Array(32); -const createMessage = () => new Domain.Message("{}", randomUUID(), ""); +const messageType = "https://didcomm.org/basicmessage/2.0/message" +const createMessage = (from?: Domain.DID, to?: Domain.DID) => new Domain.Message("{}", randomUUID(), messageType, from, to); const defaultPassword = Buffer.from(keyData); describe("Pluto + Dexie encrypted integration for browsers", () => { @@ -60,6 +61,43 @@ describe("Pluto + Dexie encrypted integration for browsers", () => { const privateKey = Fixtures.secp256K1.privateKey; await db.storePrismDID(did, 0, privateKey); expect((await db.getAllPrismDIDs()).length).toBe(1); + expect(await db.getDIDInfoByDID(did)).not.toBe(null) + }); + + it("Should return null when no privateKey is found by its id", async () => { + const did = Domain.DID.fromString( + "did:prism:733e594871d7700d35e6116011a08fc11e88ff9d366d8b5571ffc1aa18d249ea:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpxJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpw" + ); + expect(await db.getDIDInfoByDID(did)).toBe(null) + }) + + it("Should return null when no privateKey is found by its id", async () => { + expect(await db.getDIDPrivateKeyByID("not fund")).toBe(null) + }) + + it("Should return null when no privateKey is found by its did", async () => { + const did = Domain.DID.fromString( + "did:prism::t8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpxJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpw" + ); + expect((await db.getDIDPrivateKeysByDID(did)).length).toBe(0) + }) + + it("Should store a new Prism DID and its privateKeys with privateKeyMetadataId", async () => { + const did = Domain.DID.fromString( + "did:prism:733e594871d7700d35e6116011a08fc11e88ff9d366d8b5571ffc1aa18d249ea:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpxJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpw" + ); + const privateKey = Fixtures.secp256K1.privateKey; + await db.storePrismDID(did, 0, privateKey, did.toString()); + expect((await db.getAllPrismDIDs()).length).toBe(1); + }); + + it("Should store a new Prism DID and its privateKeys with privateKeyMetadataId and alias", async () => { + const did = Domain.DID.fromString( + "did:prism:733e594871d7700d35e6116011a08fc11e88ff9d366d8b5571ffc1aa18d249ea:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpxJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpw" + ); + const privateKey = Fixtures.secp256K1.privateKey; + await db.storePrismDID(did, 0, privateKey, did.toString(), "defaultalias"); + expect((await db.getAllPrismDIDs()).length).toBe(1); }); it("Should store a new Prism DID and its privateKeys and fetch it by its alias", async () => { @@ -72,7 +110,7 @@ describe("Pluto + Dexie encrypted integration for browsers", () => { expect((await db.getDIDInfoByAlias(alias)).length).toBe(1); }); - it("Should store a Message", async () => { + it("Should store a Message and fetch it", async () => { const message = createMessage(); await db.storeMessage(message); const dbMesaage = await db.getMessage(message.id); @@ -80,6 +118,26 @@ describe("Pluto + Dexie encrypted integration for browsers", () => { expect(dbMesaage!.id).toBe(message.id); }); + it("Should get all the messages", async () => { + const allMessages = await db.getAllMessages(); + expect(allMessages.length).toBe(0); + }) + + it("Should fetch stored messages either by type or by did", async () => { + const from = Domain.DID.fromString("did:prism:123456"); + const to = Domain.DID.fromString("did:prism:654321") + const from2 = Domain.DID.fromString("did:prism:12345644"); + const to2 = Domain.DID.fromString("did:prism:65432133") + + await db.storeMessages([createMessage(from, to), createMessage(from2, to2)]); + + const byType = await db.getAllMessagesOfType(messageType) + expect(byType.length).toBe(2); + + const byType2 = await db.getAllMessagesOfType(messageType, from) + expect(byType2.length).toBe(1); + }) + it("Should return null if message is not found by id ", async () => { const dbMesaage = await db.getMessage("notfound"); expect(dbMesaage).toBe(null); @@ -101,6 +159,17 @@ describe("Pluto + Dexie encrypted integration for browsers", () => { ]); }); + it("Should store private keys", async () => { + const did = new Domain.DID( + "did", + "peer", + "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOiJodHRwczovL21lZGlhdG9yLnJvb3RzaWQuY2xvdWQiLCJhIjpbImRpZGNvbW0vdjIiXX0" + ); + await db.storePrivateKeys(Fixtures.ed25519.privateKey, did, 0); + await db.storePrivateKeys(Fixtures.ed25519.privateKey, did, 0, "id2234"); + + }); + it("Should store a didPair", async () => { const host = Domain.DID.fromString("did:prism:123456"); const receiver = Domain.DID.fromString("did:prism:654321"); @@ -120,9 +189,22 @@ describe("Pluto + Dexie encrypted integration for browsers", () => { it("Should get a did pair by its did", async () => { const host = Domain.DID.fromString("did:prism:123456"); const receiver = Domain.DID.fromString("did:prism:654321"); + const notfound = Domain.DID.fromString("did:prism:65432155555"); + const name = "example"; await db.storeDIDPair(host, receiver, name); expect(await db.getPairByDID(host)).not.toBe(null); + expect(await db.getPairByDID(notfound)).toBe(null); + + }); + + it("Should get a did pair by its name", async () => { + const host = Domain.DID.fromString("did:prism:123456"); + const receiver = Domain.DID.fromString("did:prism:654321"); + const name = "example"; + await db.storeDIDPair(host, receiver, name); + expect(await db.getPairByName(name)).not.toBe(null); + expect(await db.getPairByName(" ")).toBe(null); }); it("Should store a mediator", async () => {