From 07bcddb399d908fd431f4769dd0e48f1f03d9e68 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Wed, 19 Apr 2023 22:04:27 -0500 Subject: [PATCH] `MessagesGet` (#313) * implement `MessagesGet` --- README.md | 2 +- build/compile-validators.js | 4 +- json-schemas/messages/messages-get.json | 44 +++ karma.conf.cjs | 18 +- package-lock.json | 21 ++ package.json | 3 +- src/core/message.ts | 1 + src/dwn.ts | 11 + src/index.ts | 2 + .../messages/handlers/messages-get.ts | 88 ++++++ .../messages/messages/messages-get.ts | 53 ++++ src/interfaces/messages/types.ts | 24 ++ tests/dwn.spec.ts | 36 +++ .../messages/handlers/messages-get.spec.ts | 273 ++++++++++++++++++ .../messages/messages/messages-get.spec.ts | 99 +++++++ tests/utils/test-data-generator.ts | 32 ++ 16 files changed, 705 insertions(+), 6 deletions(-) create mode 100644 json-schemas/messages/messages-get.json create mode 100644 src/interfaces/messages/handlers/messages-get.ts create mode 100644 src/interfaces/messages/messages/messages-get.ts create mode 100644 src/interfaces/messages/types.ts create mode 100644 tests/interfaces/messages/handlers/messages-get.spec.ts create mode 100644 tests/interfaces/messages/messages/messages-get.spec.ts diff --git a/README.md b/README.md index bb6791eb6..8f562b4ee 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-93.69%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.17%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.36%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.69%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-93.86%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.36%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.54%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.86%25-brightgreen.svg?style=flat) ## Introduction diff --git a/build/compile-validators.js b/build/compile-validators.js index 1971a3a4b..d01b7b800 100644 --- a/build/compile-validators.js +++ b/build/compile-validators.js @@ -22,7 +22,8 @@ import EventsGet from '../json-schemas/events/events-get.json' assert { type: 'j import GeneralJwk from '../json-schemas/jwk/general-jwk.json' assert { type: 'json' }; import GeneralJws from '../json-schemas/general-jws.json' assert { type: 'json' }; import HooksWrite from '../json-schemas/hooks/hooks-write.json' assert { type: 'json' }; -import JwkVerificationMethod from '../json-schemas/jwk-verification-method.json' assert {type: 'json'}; +import JwkVerificationMethod from '../json-schemas/jwk-verification-method.json' assert { type: 'json' }; +import MessagesGet from '../json-schemas/messages/messages-get.json' assert { type: 'json' }; import PermissionsDefinitions from '../json-schemas/permissions/definitions.json' assert { type: 'json' }; import PermissionsGrant from '../json-schemas/permissions/permissions-grant.json' assert { type: 'json' }; import PermissionsRequest from '../json-schemas/permissions/permissions-request.json' assert { type: 'json' }; @@ -46,6 +47,7 @@ const schemas = { GeneralJws, HooksWrite, JwkVerificationMethod, + MessagesGet, PermissionsDefinitions, PermissionsGrant, PermissionsRequest, diff --git a/json-schemas/messages/messages-get.json b/json-schemas/messages/messages-get.json new file mode 100644 index 000000000..017b913c3 --- /dev/null +++ b/json-schemas/messages/messages-get.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://identity.foundation/dwn/json-schemas/messages-get.json", + "type": "object", + "additionalProperties": false, + "required": [ + "authorization", + "descriptor" + ], + "properties": { + "authorization": { + "$ref": "https://identity.foundation/dwn/json-schemas/general-jws.json" + }, + "descriptor": { + "type": "object", + "additionalProperties": false, + "required": [ + "interface", + "method" + ], + "properties": { + "interface": { + "enum": [ + "Messages" + ], + "type": "string" + }, + "method": { + "enum": [ + "Get" + ], + "type": "string" + }, + "messageCids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + } + } + } +} \ No newline at end of file diff --git a/karma.conf.cjs b/karma.conf.cjs index afaa6eb9e..a85268846 100644 --- a/karma.conf.cjs +++ b/karma.conf.cjs @@ -5,10 +5,23 @@ const playwright = require('playwright'); const esbuildBrowserConfig = require('./build/esbuild-browser-config.cjs'); -// set playwright as run-target for webkit tests +// use playwright chrome exec path as run target for chromium tests +process.env.CHROME_BIN = playwright.chromium.executablePath(); + +// use playwright webkit exec path as run target for safari tests process.env.WEBKIT_HEADLESS_BIN = playwright.webkit.executablePath(); -module.exports = function(config) { +// use playwright firefox exec path as run target for firefox tests +process.env.FIREFOX_BIN = playwright.firefox.executablePath(); + +/** @typedef {import('karma').Config} KarmaConfig */ + +/** + * + * @param {KarmaConfig} config + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +module.exports = function configure(config) { config.set({ plugins: [ require('karma-chrome-launcher'), @@ -23,7 +36,6 @@ module.exports = function(config) { // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter frameworks: ['mocha'], - // list of files / patterns to load in the browser files: [ { pattern: 'tests/**/*.spec.ts', watched: false } diff --git a/package-lock.json b/package-lock.json index e540cf6ff..2599925bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@types/chai": "4.3.0", "@types/chai-as-promised": "7.1.5", "@types/flat": "^5.0.2", + "@types/karma": "^6.3.3", "@types/lodash": "4.14.179", "@types/mocha": "9.1.0", "@types/randombytes": "2.0.0", @@ -902,6 +903,16 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/karma": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.3.tgz", + "integrity": "sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==", + "dev": true, + "dependencies": { + "@types/node": "*", + "log4js": "^6.4.1" + } + }, "node_modules/@types/lodash": { "version": "4.14.179", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz", @@ -8558,6 +8569,16 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/karma": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.3.tgz", + "integrity": "sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==", + "dev": true, + "requires": { + "@types/node": "*", + "log4js": "^6.4.1" + } + }, "@types/lodash": { "version": "4.14.179", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz", diff --git a/package.json b/package.json index 269ef184e..11f05b8c6 100644 --- a/package.json +++ b/package.json @@ -77,8 +77,8 @@ "multiformats": "11.0.2", "randombytes": "2.1.0", "readable-stream": "4.3.0", - "ulid": "2.3.0", "secp256k1": "5.0.0", + "ulid": "2.3.0", "uuid": "8.3.2", "varint": "6.0.0" }, @@ -86,6 +86,7 @@ "@types/chai": "4.3.0", "@types/chai-as-promised": "7.1.5", "@types/flat": "^5.0.2", + "@types/karma": "^6.3.3", "@types/lodash": "4.14.179", "@types/mocha": "9.1.0", "@types/randombytes": "2.0.0", diff --git a/src/core/message.ts b/src/core/message.ts index 28850b92d..bcfbed5af 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -11,6 +11,7 @@ import { validateJsonSchema } from '../schema-validator.js'; export enum DwnInterfaceName { Events = 'Events', Hooks = 'Hooks', + Messages = 'Messages', Permissions = 'Permissions', Protocols = 'Protocols', Records = 'Records' diff --git a/src/dwn.ts b/src/dwn.ts index a8705c1b5..4233cae4d 100644 --- a/src/dwn.ts +++ b/src/dwn.ts @@ -5,6 +5,7 @@ import type { MessageStore } from './store/message-store.js'; import type { MethodHandler } from './interfaces/types.js'; import type { Readable } from 'readable-stream'; import type { TenantGate } from './core/tenant-gate.js'; +import type { MessagesGetMessage, MessagesGetReply } from './interfaces/messages/types.js'; import type { RecordsReadMessage, RecordsReadReply } from './interfaces/records/types.js'; import { AllowAllTenantGate } from './core/tenant-gate.js'; @@ -13,6 +14,7 @@ import { DidResolver } from './did/did-resolver.js'; import { EventLogLevel } from './event-log/event-log-level.js'; import { EventsGetHandler } from './interfaces/events/handlers/events-get.js'; import { MessageReply } from './core/message-reply.js'; +import { MessagesGetHandler } from './interfaces/messages/handlers/messages-get.js'; import { MessageStoreLevel } from './store/message-store-level.js'; import { PermissionsRequestHandler } from './interfaces/permissions/handlers/permissions-request.js'; import { ProtocolsConfigureHandler } from './interfaces/protocols/handlers/protocols-configure.js'; @@ -40,6 +42,7 @@ export class Dwn { this.methodHandlers = { [DwnInterfaceName.Events + DwnMethodName.Get] : new EventsGetHandler(this.didResolver, this.eventLog), + [DwnInterfaceName.Messages + DwnMethodName.Get] : new MessagesGetHandler(this.didResolver, this.messageStore, this.dataStore), [DwnInterfaceName.Permissions + DwnMethodName.Request] : new PermissionsRequestHandler(this.didResolver, this.messageStore, this.dataStore), [DwnInterfaceName.Protocols + DwnMethodName.Configure] : new ProtocolsConfigureHandler( this.didResolver, this.messageStore, this.dataStore, this.eventLog), @@ -126,6 +129,14 @@ export class Dwn { return reply as RecordsReadReply; } + /** + * Handles a `MessagesGet` message. + */ + public async handleMessagesGet(tenant: string, message: MessagesGetMessage): Promise { + const reply = await this.processMessage(tenant, message); + return reply as MessagesGetReply; + } + public async dump(): Promise { console.group('didResolver'); await this.didResolver['dump']?.(); diff --git a/src/index.ts b/src/index.ts index 7a35fe17c..afeaa0b32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export type { DwnServiceEndpoint, ServiceEndpoint, DidDocument, DidResolutionRes export type { EventLog, Event } from './event-log/event-log.js'; export type { EventsGetMessage, EventsGetReply } from './interfaces/events/types.js'; export type { HooksWriteMessage } from './interfaces/hooks/types.js'; +export type { MessagesGetMessage, MessagesGetReply } from './interfaces/messages/types.js'; export type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage, ProtocolsQueryMessage } from './interfaces/protocols/types.js'; export type { RecordsDeleteMessage, RecordsQueryMessage, RecordsWriteMessage } from './interfaces/records/types.js'; export { AllowAllTenantGate, TenantGate } from './core/tenant-gate.js'; @@ -31,6 +32,7 @@ export { EncryptionInput, KeyEncryptionInput, RecordsWrite, RecordsWriteOptions, export { HooksWrite, HooksWriteOptions } from './interfaces/hooks/messages/hooks-write.js'; export { Jws } from './utils/jws.js'; export { KeyMaterial, PrivateJwk, PublicJwk } from './jose/types.js'; +export { MessagesGet, MessagesGetOptions } from './interfaces/messages/messages/messages-get.js'; export { MessageReply } from './core/message-reply.js'; export { MessageStore } from './store/message-store.js'; export { MessageStoreLevel } from './store/message-store-level.js'; diff --git a/src/interfaces/messages/handlers/messages-get.ts b/src/interfaces/messages/handlers/messages-get.ts new file mode 100644 index 000000000..bd505f54e --- /dev/null +++ b/src/interfaces/messages/handlers/messages-get.ts @@ -0,0 +1,88 @@ +import type { DataStore } from '../../../store/data-store.js'; +import type { DidResolver } from '../../../did/did-resolver.js'; +import type { MessageStore } from '../../../store/message-store.js'; +import type { MethodHandler } from '../../types.js'; +import type { MessagesGetMessage, MessagesGetReply, MessagesGetReplyEntry } from '../types.js'; + +import { DataStream } from '../../../utils/data-stream.js'; +import { DwnConstant } from '../../../core/dwn-constant.js'; +import { Encoder } from '../../../utils/encoder.js'; +import { MessageReply } from '../../../core/message-reply.js'; +import { MessagesGet } from '../messages/messages-get.js'; +import { authenticate, authorize } from '../../../core/auth.js'; +import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js'; + +type HandleArgs = { tenant: string, message: MessagesGetMessage }; + +export class MessagesGetHandler implements MethodHandler { + constructor(private didResolver: DidResolver, private messageStore: MessageStore, private dataStore: DataStore) {} + + public async handle({ tenant, message }: HandleArgs): Promise { + let messagesGet: MessagesGet; + + try { + messagesGet = await MessagesGet.parse(message); + } catch (e) { + return MessageReply.fromError(e, 400); + } + + try { + await authenticate(message.authorization, this.didResolver); + await authorize(tenant, messagesGet); + } catch (e) { + return MessageReply.fromError(e, 401); + } + + const promises: Promise[] = []; + const messageCids = new Set(message.descriptor.messageCids); + + for (const messageCid of messageCids) { + const promise = this.messageStore.get(tenant, messageCid) + .then(message => { + return { messageCid, message }; + }) + .catch(_ => { + return { messageCid, message: undefined, error: `Failed to get message ${messageCid}` }; + }); + + promises.push(promise); + } + + const messages = await Promise.all(promises); + + // for every message, include associated data as `encodedData` IF: + // * its a RecordsWrite + // * the data size is equal or smaller than the size threshold + //! NOTE: this is somewhat duplicate code that also exists in `StorageController.query`. + for (const entry of messages) { + const { message } = entry; + + if (!message) { + continue; + } + + const { interface: messageInterface, method } = message.descriptor; + if (messageInterface !== DwnInterfaceName.Records || method !== DwnMethodName.Write) { + continue; + } + + const dataCid = message.descriptor.dataCid; + const dataSize = message.descriptor.dataSize; + + if (dataCid !== undefined && dataSize! <= DwnConstant.maxDataSizeAllowedToBeEncoded) { + const messageCid = await Message.getCid(message); + const result = await this.dataStore.get(tenant, messageCid, dataCid); + + if (result) { + const dataBytes = await DataStream.toBytes(result.dataStream); + entry.encodedData = Encoder.bytesToBase64Url(dataBytes); + } + } + } + + return { + status: { code: 200, detail: 'OK' }, + messages + }; + } +} \ No newline at end of file diff --git a/src/interfaces/messages/messages/messages-get.ts b/src/interfaces/messages/messages/messages-get.ts new file mode 100644 index 000000000..da8ed7d44 --- /dev/null +++ b/src/interfaces/messages/messages/messages-get.ts @@ -0,0 +1,53 @@ +import type { SignatureInput } from '../../../jose/jws/general/types.js'; +import type { MessagesGetDescriptor, MessagesGetMessage } from '../types.js'; + +import { parseCid } from '../../../utils/cid.js'; +import { validateAuthorizationIntegrity } from '../../../core/auth.js'; +import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js'; + +export type MessagesGetOptions = { + messageCids: string[]; + authorizationSignatureInput: SignatureInput; +}; + +export class MessagesGet extends Message { + public static async parse(message: MessagesGetMessage): Promise { + Message.validateJsonSchema(message); + this.validateMessageCids(message.descriptor.messageCids); + + await validateAuthorizationIntegrity(message); + + return new MessagesGet(message); + } + + public static async create(options: MessagesGetOptions): Promise { + const descriptor: MessagesGetDescriptor = { + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Get, + messageCids : options.messageCids + }; + + const authorization = await Message.signAsAuthorization(descriptor, options.authorizationSignatureInput); + const message = { descriptor, authorization }; + + Message.validateJsonSchema(message); + MessagesGet.validateMessageCids(options.messageCids); + + return new MessagesGet(message); + } + + /** + * validates the provided cids + * @param messageCids - the cids in question + * @throws {Error} if an invalid cid is found. + */ + private static validateMessageCids(messageCids: string[]): void { + for (const cid of messageCids) { + try { + parseCid(cid); + } catch (_) { + throw new Error(`${cid} is not a valid CID`); + } + } + } +} \ No newline at end of file diff --git a/src/interfaces/messages/types.ts b/src/interfaces/messages/types.ts new file mode 100644 index 000000000..4657d77e5 --- /dev/null +++ b/src/interfaces/messages/types.ts @@ -0,0 +1,24 @@ +import type { BaseMessage } from '../../core/types.js'; +import type { BaseMessageReply } from '../../core/message-reply.js'; +import type { DwnInterfaceName, DwnMethodName } from '../../core/message.js'; + +export type MessagesGetDescriptor = { + interface : DwnInterfaceName.Messages; + method: DwnMethodName.Get; + messageCids: string[]; +}; + +export type MessagesGetMessage = BaseMessage & { + descriptor: MessagesGetDescriptor; +}; + +export type MessagesGetReplyEntry = { + messageCid: string; + message?: BaseMessage; + encodedData?: string; + error?: string; +}; + +export type MessagesGetReply = BaseMessageReply & { + messages?: MessagesGetReplyEntry[]; +}; \ No newline at end of file diff --git a/tests/dwn.spec.ts b/tests/dwn.spec.ts index 923854b2e..b0c17a72c 100644 --- a/tests/dwn.spec.ts +++ b/tests/dwn.spec.ts @@ -161,4 +161,40 @@ describe('DWN', () => { expect(reply.status.detail).to.contain('not a tenant'); }); }); + + describe('handleMessagesGet', () => { + it('increases test coverage :)', async () => { + const did = await DidKeyResolver.generate(); + const alice = await TestDataGenerator.generatePersona(did); + const messageCids: string[] = []; + + const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ + requester: alice + }); + + const messageCid = await Message.getCid(recordsWrite.message); + messageCids.push(messageCid); + + const reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), dataStream); + expect(reply.status.code).to.equal(202); + + const { messagesGet } = await TestDataGenerator.generateMessagesGet({ + requester: alice, + messageCids + }); + + const messagesGetReply = await dwn.handleMessagesGet(alice.did, messagesGet.message); + expect(messagesGetReply.status.code).to.equal(200); + expect(messagesGetReply.messages!.length).to.equal(messageCids.length); + + for (const messageReply of messagesGetReply.messages!) { + expect(messageReply.messageCid).to.not.be.undefined; + expect(messageReply.message).to.not.be.undefined; + expect(messageCids).to.include(messageReply.messageCid); + + const cid = await Message.getCid(messageReply.message!); + expect(messageReply.messageCid).to.equal(cid); + } + }); + }); }); diff --git a/tests/interfaces/messages/handlers/messages-get.spec.ts b/tests/interfaces/messages/handlers/messages-get.spec.ts new file mode 100644 index 000000000..9039e0cb1 --- /dev/null +++ b/tests/interfaces/messages/handlers/messages-get.spec.ts @@ -0,0 +1,273 @@ +import type { MessagesGetReply } from '../../../../src/index.js'; + +import { expect } from 'chai'; +import { Message } from '../../../../src/core/message.js'; +import { MessagesGetHandler } from '../../../../src/interfaces/messages/handlers/messages-get.js'; +import { TestDataGenerator } from '../../../utils/test-data-generator.js'; +import { + DataStoreLevel, + DidKeyResolver, + DidResolver, + Dwn, + EventLogLevel, + MessageStoreLevel +} from '../../../../src/index.js'; + +import sinon from 'sinon'; + +describe('MessagesGetHandler.handle()', () => { + let dwn: Dwn; + let didResolver: DidResolver; + let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; + let eventLog: EventLogLevel; + + before(async () => { + didResolver = new DidResolver([new DidKeyResolver()]); + + // important to follow this pattern to initialize and clean the message and data store in tests + // so that different suites can reuse the same block store and index location for testing + messageStore = new MessageStoreLevel({ + blockstoreLocation : 'TEST-MESSAGESTORE', + indexLocation : 'TEST-INDEX' + }); + + dataStore = new DataStoreLevel({ + blockstoreLocation: 'TEST-DATASTORE' + }); + + eventLog = new EventLogLevel({ + location: 'TEST-EVENTLOG' + }); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore, eventLog }); + }); + + beforeEach(async () => { + // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + await messageStore.clear(); + await dataStore.clear(); + await eventLog.clear(); + + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + }); + + after(async () => { + await dwn.close(); + }); + + it('returns a 401 if tenant is not author', async () => { + const alice = await DidKeyResolver.generate(); + const bob = await DidKeyResolver.generate(); + const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); + + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : alice, + messageCids : [await Message.getCid(recordsWrite.message)] + }); + + const reply = await dwn.processMessage(bob.did, message); + + expect(reply.status.code).to.equal(401); + expect(reply.entries).to.not.exist; + expect(reply.data).to.not.exist; + }); + + it('returns a 400 if message is invalid', async () => { + const alice = await DidKeyResolver.generate(); + const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); + + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : alice, + messageCids : [await Message.getCid(recordsWrite.message)] + }); + + message['descriptor']['troll'] = 'hehe'; + + const reply = await dwn.processMessage(alice.did, message); + + expect(reply.status.code).to.equal(400); + expect(reply.entries).to.not.exist; + expect(reply.data).to.not.exist; + }); + + it('returns a 400 if message contains an invalid message cid', async () => { + const alice = await DidKeyResolver.generate(); + const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); + + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : alice, + messageCids : [await Message.getCid(recordsWrite.message)] + }); + + message.descriptor.messageCids = ['hehetroll']; + + const reply: MessagesGetReply = await dwn.processMessage(alice.did, message); + + expect(reply.status.code).to.equal(400); + expect(reply.status.detail).to.include('is not a valid CID'); + expect(reply.messages).to.be.undefined; + }); + + it('returns all requested messages', async () => { + const did = await DidKeyResolver.generate(); + const alice = await TestDataGenerator.generatePersona(did); + const messageCids: string[] = []; + + const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ + requester: alice + }); + + let messageCid = await Message.getCid(recordsWrite.message); + messageCids.push(messageCid); + + let reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), dataStream); + expect(reply.status.code).to.equal(202); + + const { recordsDelete } = await TestDataGenerator.generateRecordsDelete({ + requester : alice, + recordId : recordsWrite.message.recordId + }); + + messageCid = await Message.getCid(recordsDelete.message); + messageCids.push(messageCid); + + reply = await dwn.processMessage(alice.did, recordsDelete.toJSON()); + expect(reply.status.code).to.equal(202); + + const { protocolsConfigure } = await TestDataGenerator.generateProtocolsConfigure({ + requester: alice + }); + + messageCid = await Message.getCid(protocolsConfigure.message); + messageCids.push(messageCid); + + reply = await dwn.processMessage(alice.did, protocolsConfigure.toJSON()); + expect(reply.status.code).to.equal(202); + + const { messagesGet } = await TestDataGenerator.generateMessagesGet({ + requester: alice, + messageCids + }); + + const messagesGetReply: MessagesGetReply = await dwn.processMessage(alice.did, messagesGet.toJSON()); + expect(messagesGetReply.status.code).to.equal(200); + expect(messagesGetReply.messages!.length).to.equal(messageCids.length); + + for (const messageReply of messagesGetReply.messages!) { + expect(messageReply.messageCid).to.not.be.undefined; + expect(messageReply.message).to.not.be.undefined; + expect(messageCids).to.include(messageReply.messageCid); + + const cid = await Message.getCid(messageReply.message!); + expect(messageReply.messageCid).to.equal(cid); + } + }); + + it('returns message as undefined in reply entry when a messageCid is not found', async () => { + const alice = await DidKeyResolver.generate(); + const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); + const recordsWriteMessageCid = await Message.getCid(recordsWrite.message); + + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : alice, + messageCids : [recordsWriteMessageCid] + }); + + // 0 messages expected because the RecordsWrite created above was never stored + const reply: MessagesGetReply = await dwn.processMessage(alice.did, message); + expect(reply.status.code).to.equal(200); + expect(reply.messages!.length).to.equal(1); + + for (const messageReply of reply.messages!) { + expect(messageReply.messageCid).to.equal(recordsWriteMessageCid); + expect(messageReply.message).to.be.undefined; + } + }); + + it('returns an error message for a specific cid if getting that message from the MessageStore fails', async () => { + // stub the messageStore.get call to throw an error + const messageStore = sinon.createStubInstance(MessageStoreLevel); + messageStore.get.rejects('internal db error'); + + const dataStore = sinon.createStubInstance(DataStoreLevel); + + const messagesGetHandler = new MessagesGetHandler(didResolver, messageStore, dataStore); + + const alice = await DidKeyResolver.generate(); + const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); + const recordsWriteMessageCid = await Message.getCid(recordsWrite.message); + + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : alice, + messageCids : [recordsWriteMessageCid] + }); + + const reply = await messagesGetHandler.handle({ tenant: alice.did, message }); + + expect(messageStore.get.called).to.be.true; + + expect(reply.status.code).to.equal(200); + expect(reply.messages!.length).to.equal(1); + expect(reply.messages![0].error).to.exist; + expect(reply.messages![0].error).to.include(`Failed to get message ${recordsWriteMessageCid}`); + expect(reply.messages![0].message).to.be.undefined; + }); + + it('includes encodedData in reply entry if the data is available and dataSize < threshold', async () => { + const alice = await DidKeyResolver.generate(); + + const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ + requester: alice + }); + + const reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), dataStream); + expect(reply.status.code).to.equal(202); + + const recordsWriteMessageCid = await Message.getCid(recordsWrite.message); + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : alice, + messageCids : [recordsWriteMessageCid] + }); + + const messagesGetReply: MessagesGetReply = await dwn.processMessage(alice.did, message); + expect(messagesGetReply.status.code).to.equal(200); + expect(messagesGetReply.messages!.length).to.equal(1); + + for (const messageReply of messagesGetReply.messages!) { + expect(messageReply.messageCid).to.exist; + expect(messageReply.messageCid).to.equal(recordsWriteMessageCid); + + expect(messageReply.message).to.exist.and.not.be.undefined; + expect(messageReply.encodedData).to.exist.and.not.be.undefined; + } + }); + + it('does not return messages that belong to other tenants', async () => { + const alice = await DidKeyResolver.generate(); + const bob = await DidKeyResolver.generate(); + + const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ + requester: alice + }); + + const reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), dataStream); + expect(reply.status.code).to.equal(202); + + const recordsWriteMessageCid = await Message.getCid(recordsWrite.message); + const { message } = await TestDataGenerator.generateMessagesGet({ + requester : bob, + messageCids : [await Message.getCid(recordsWrite.message)] + }); + + // 0 messages expected because the RecordsWrite created above is not bob's + const messagesGetReply: MessagesGetReply = await dwn.processMessage(bob.did, message); + expect(messagesGetReply.status.code).to.equal(200); + expect(messagesGetReply.messages!.length).to.equal(1); + + for (const messageReply of messagesGetReply.messages!) { + expect(messageReply.messageCid).to.equal(recordsWriteMessageCid); + expect(messageReply.message).to.be.undefined; + } + }); +}); \ No newline at end of file diff --git a/tests/interfaces/messages/messages/messages-get.spec.ts b/tests/interfaces/messages/messages/messages-get.spec.ts new file mode 100644 index 000000000..74ba1e6d8 --- /dev/null +++ b/tests/interfaces/messages/messages/messages-get.spec.ts @@ -0,0 +1,99 @@ +import type { MessagesGetMessage } from '../../../../src/index.js'; + +import { expect } from 'chai'; +import { Jws } from '../../../../src/index.js'; +import { Message } from '../../../../src/core/message.js'; +import { MessagesGet } from '../../../../src/index.js'; +import { TestDataGenerator } from '../../../utils/test-data-generator.js'; + +describe('MessagesGet Message', () => { + describe('create', () => { + it('creates a MessagesGet message', async () => { + const { requester, message } = await TestDataGenerator.generateRecordsWrite(); + const messageCid = await Message.getCid(message); + + const messagesGet = await MessagesGet.create({ + authorizationSignatureInput : await Jws.createSignatureInput(requester), + messageCids : [messageCid] + }); + + expect(messagesGet.message.authorization).to.exist; + expect(messagesGet.message.descriptor).to.exist; + expect(messagesGet.message.descriptor.messageCids.length).to.equal(1); + expect(messagesGet.message.descriptor.messageCids).to.include(messageCid); + }); + + + it('throws an error if at least 1 message cid isnt provided', async () => { + const alice = await TestDataGenerator.generatePersona(); + + try { + await MessagesGet.create({ + authorizationSignatureInput : await Jws.createSignatureInput(alice), + messageCids : [] + }); + + expect.fail(); + } catch (e: any) { + // error message auto-generated by AJV + expect(e.message).to.include('/descriptor/messageCids: must NOT have fewer than 1 items'); + } + }); + + it('throws an error if an invalid CID is provided', async () => { + const alice = await TestDataGenerator.generatePersona(); + + try { + await MessagesGet.create({ + authorizationSignatureInput : await Jws.createSignatureInput(alice), + messageCids : ['abcd'] + }); + + expect.fail(); + } catch (e: any) { + expect(e.message).to.include('is not a valid CID'); + } + }); + }); + + describe('parse', () => { + it('parses a message into a MessagesGet instance', async () => { + const { requester, message } = await TestDataGenerator.generateRecordsWrite(); + let messageCid = await Message.getCid(message); + + const messagesGet = await MessagesGet.create({ + authorizationSignatureInput : await Jws.createSignatureInput(requester), + messageCids : [messageCid] + }); + + const parsed = await MessagesGet.parse(messagesGet.message); + expect(parsed).to.be.instanceof(MessagesGet); + + const expectedMessageCid = await Message.getCid(messagesGet.message); + messageCid = await Message.getCid(parsed.message); + + expect(messageCid).to.equal(expectedMessageCid); + }); + + it('throws an exception if messageCids contains an invalid cid', async () => { + const { requester, message: recordsWriteMessage } = await TestDataGenerator.generateRecordsWrite(); + const messageCid = await Message.getCid(recordsWriteMessage); + + const messagesGet = await MessagesGet.create({ + authorizationSignatureInput : await Jws.createSignatureInput(requester), + messageCids : [messageCid] + }); + + const message = messagesGet.toJSON() as MessagesGetMessage; + message.descriptor.messageCids = ['abcd']; + + try { + await MessagesGet.parse(message); + + expect.fail(); + } catch (e: any) { + expect(e.message).to.include('is not a valid CID'); + } + }); + }); +}); \ No newline at end of file diff --git a/tests/utils/test-data-generator.ts b/tests/utils/test-data-generator.ts index 01f844538..7a8be80cc 100644 --- a/tests/utils/test-data-generator.ts +++ b/tests/utils/test-data-generator.ts @@ -9,6 +9,8 @@ import type { EventsGetOptions, HooksWriteMessage, HooksWriteOptions, + MessagesGetMessage, + MessagesGetOptions, ProtocolDefinition, ProtocolsConfigureMessage, ProtocolsConfigureOptions, @@ -37,6 +39,7 @@ import { EventsGet, HooksWrite, Jws, + MessagesGet, ProtocolsConfigure, ProtocolsQuery, RecordsDelete, @@ -175,6 +178,17 @@ export type GenerateEventsGetOutput = { message: EventsGetMessage; }; +export type GenerateMessagesGetInput = { + requester?: Persona; + messageCids: string[] +}; + +export type GenerateMessagesGetOutput = { + requester: Persona; + message: MessagesGetMessage; + messagesGet: MessagesGet; +}; + /** * Utility class for generating data for testing. */ @@ -470,6 +484,24 @@ export class TestDataGenerator { }; } + public static async generateMessagesGet(input: GenerateMessagesGetInput): Promise { + const requester = input?.requester ?? await TestDataGenerator.generatePersona(); + const authorizationSignatureInput = Jws.createSignatureInput(requester); + + const options: MessagesGetOptions = { + authorizationSignatureInput, + messageCids: input.messageCids + }; + + const messagesGet = await MessagesGet.create(options); + + return { + requester, + messagesGet, + message: messagesGet.message, + }; + } + /** * Generates a random alpha-numeric string. */