diff --git a/.gitignore b/.gitignore index 077df4773..fcb504825 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,10 @@ try.ts # locally used file as a js playground try.js # default location for levelDB data storage in a non-browser env +DATASTORE BLOCKSTORE # location for levelDB data storage for non-browser tests +TEST-DATASTORE TEST-BLOCKSTORE # default location for index specific levelDB data storage in a non-browser env INDEX diff --git a/README.md b/README.md index d1f73b27e..307076742 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-95.15%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.19%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.17%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-95.15%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-94.36%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.95%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.3%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.36%25-brightgreen.svg?style=flat) ## Introduction diff --git a/package-lock.json b/package-lock.json index 72724f4fc..6a59a52ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ "@noble/secp256k1": "1.7.1", "@swc/helpers": "0.3.8", "@types/ms": "0.7.31", + "@types/node": "^18.13.0", + "@types/readable-stream": "^2.3.15", + "@types/search-index": "3.2.0", "ajv": "8.11.0", "cross-fetch": "3.1.5", "date-fns": "2.28.0", @@ -39,8 +42,6 @@ "@types/lodash": "4.14.179", "@types/mocha": "9.1.0", "@types/randombytes": "2.0.0", - "@types/readable-stream": "^2.3.15", - "@types/search-index": "3.2.0", "@types/sinon": "10.0.11", "@types/varint": "6.0.0", "@typescript-eslint/eslint-plugin": "5.37.0", @@ -806,8 +807,7 @@ "node_modules/@types/abstract-leveldown": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", - "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==", - "dev": true + "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==" }, "node_modules/@types/chai": { "version": "4.3.0", @@ -877,9 +877,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "18.7.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz", - "integrity": "sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==" + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==" }, "node_modules/@types/randombytes": { "version": "2.0.0", @@ -894,7 +894,6 @@ "version": "2.3.15", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", - "dev": true, "dependencies": { "@types/node": "*", "safe-buffer": "~5.1.1" @@ -903,14 +902,12 @@ "node_modules/@types/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/@types/search-index": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/search-index/-/search-index-3.2.0.tgz", "integrity": "sha512-1JO0PjageEhvU8JZoiysPU+Ng19SAo0OzGW3ItybBwkeeIdTw40hRpCacir4pL0NsoyQkVF/hBax5uNVB90YUQ==", - "dev": true, "dependencies": { "@types/abstract-leveldown": "*" } @@ -8053,8 +8050,7 @@ "@types/abstract-leveldown": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", - "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==", - "dev": true + "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==" }, "@types/chai": { "version": "4.3.0", @@ -8124,9 +8120,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "@types/node": { - "version": "18.7.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz", - "integrity": "sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==" + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==" }, "@types/randombytes": { "version": "2.0.0", @@ -8141,7 +8137,6 @@ "version": "2.3.15", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", - "dev": true, "requires": { "@types/node": "*", "safe-buffer": "~5.1.1" @@ -8150,8 +8145,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -8159,7 +8153,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/search-index/-/search-index-3.2.0.tgz", "integrity": "sha512-1JO0PjageEhvU8JZoiysPU+Ng19SAo0OzGW3ItybBwkeeIdTw40hRpCacir4pL0NsoyQkVF/hBax5uNVB90YUQ==", - "dev": true, "requires": { "@types/abstract-leveldown": "*" } diff --git a/package.json b/package.json index d354379aa..7e842e1f0 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,9 @@ "@noble/secp256k1": "1.7.1", "@swc/helpers": "0.3.8", "@types/ms": "0.7.31", + "@types/node": "^18.13.0", + "@types/readable-stream": "^2.3.15", + "@types/search-index": "3.2.0", "ajv": "8.11.0", "cross-fetch": "3.1.5", "date-fns": "2.28.0", @@ -78,8 +81,6 @@ "@types/lodash": "4.14.179", "@types/mocha": "9.1.0", "@types/randombytes": "2.0.0", - "@types/readable-stream": "^2.3.15", - "@types/search-index": "3.2.0", "@types/sinon": "10.0.11", "@types/varint": "6.0.0", "@typescript-eslint/eslint-plugin": "5.37.0", diff --git a/src/dwn.ts b/src/dwn.ts index e99346a64..2d8907662 100644 --- a/src/dwn.ts +++ b/src/dwn.ts @@ -1,34 +1,44 @@ import type { BaseMessage } from './core/types.js'; -import type { DidMethodResolver } from './did/did-resolver.js'; +import type { DataStore } from './store/data-store.js'; 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 { AllowAllTenantGate } from './core/tenant-gate.js'; +import { DataStoreLevel } from './store/data-store-level.js'; import { DidResolver } from './did/did-resolver.js'; -import { Message } from './core/message.js'; import { MessageReply } from './core/message-reply.js'; import { MessageStoreLevel } from './store/message-store-level.js'; -import { PermissionsInterface } from './interfaces/permissions/permissions-interface.js'; -import { ProtocolsInterface } from './interfaces/protocols/protocols-interface.js'; -import { RecordsInterface } from './interfaces/records/records-interface.js'; +import { PermissionsRequestHandler } from './interfaces/permissions/handlers/permissions-request.js'; +import { ProtocolsConfigureHandler } from './interfaces/protocols/handlers/protocols-configure.js'; +import { ProtocolsQueryHandler } from './interfaces/protocols/handlers/protocols-query.js'; +import { RecordsDeleteHandler } from './interfaces/records/handlers/records-delete.js'; +import { RecordsQueryHandler } from './interfaces/records/handlers/records-query.js'; +import { RecordsWriteHandler } from './interfaces/records/handlers/records-write.js'; +import { DwnInterfaceName, DwnMethodName, Message } from './core/message.js'; export class Dwn { - static methodHandlers: { [key:string]: MethodHandler } = { - ...RecordsInterface.methodHandlers, - ...PermissionsInterface.methodHandlers, - ...ProtocolsInterface.methodHandlers - }; - - private DidResolver: DidResolver; + private methodHandlers: { [key:string]: MethodHandler }; + private didResolver: DidResolver; private messageStore: MessageStore; + private dataStore: DataStore; private tenantGate: TenantGate; private constructor(config: DwnConfig) { - this.DidResolver = new DidResolver(config.didMethodResolvers); + this.didResolver = config.didResolver; this.messageStore = config.messageStore; + this.dataStore = config.dataStore; this.tenantGate = config.tenantGate; + + this.methodHandlers = { + [DwnInterfaceName.Permissions + DwnMethodName.Request] : new PermissionsRequestHandler(this.didResolver, this.messageStore, this.dataStore), + [DwnInterfaceName.Protocols + DwnMethodName.Configure] : new ProtocolsConfigureHandler(this.didResolver, this.messageStore, this.dataStore), + [DwnInterfaceName.Protocols + DwnMethodName.Query] : new ProtocolsQueryHandler(this.didResolver, this.messageStore, this.dataStore), + [DwnInterfaceName.Records + DwnMethodName.Delete] : new RecordsDeleteHandler(this.didResolver, this.messageStore, this.dataStore), + [DwnInterfaceName.Records + DwnMethodName.Query] : new RecordsQueryHandler(this.didResolver, this.messageStore, this.dataStore), + [DwnInterfaceName.Records + DwnMethodName.Write] : new RecordsWriteHandler(this.didResolver, this.messageStore, this.dataStore), + }; } /** @@ -36,8 +46,10 @@ export class Dwn { */ static async create(config?: DwnConfig): Promise { config ??= { }; + config.didResolver ??= new DidResolver(); config.tenantGate ??= new AllowAllTenantGate(); config.messageStore ??= new MessageStoreLevel(); + config.dataStore ??= new DataStoreLevel(); const dwn = new Dwn(config); await dwn.open(); @@ -46,11 +58,13 @@ export class Dwn { } private async open(): Promise { - return this.messageStore.open(); + await this.messageStore.open(); + await this.dataStore.open(); } async close(): Promise { - return this.messageStore.close(); + this.messageStore.close(); + this.dataStore.close(); } /** @@ -83,13 +97,9 @@ export class Dwn { } const handlerKey = dwnInterface + dwnMethod; - const interfaceMethodHandler = Dwn.methodHandlers[handlerKey]; - - const methodHandlerReply = await interfaceMethodHandler({ + const methodHandlerReply = await this.methodHandlers[handlerKey].handle({ tenant, - message : rawMessage as BaseMessage, - messageStore : this.messageStore, - didResolver : this.DidResolver, + message: rawMessage as BaseMessage, dataStream }); return methodHandlerReply; @@ -97,7 +107,8 @@ export class Dwn { }; export type DwnConfig = { - didMethodResolvers?: DidMethodResolver[], + didResolver?: DidResolver, messageStore?: MessageStore; + dataStore?: DataStore; tenantGate?: TenantGate; }; diff --git a/src/index.ts b/src/index.ts index 04ea37c47..588669142 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export type { HooksWriteMessage } from './interfaces/hooks/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'; +export { DataStore } from './store/data-store.js'; export { DateSort } from './interfaces/records/messages/records-query.js'; export { DataStream } from './utils/data-stream.js'; export { DidKeyResolver } from './did/did-key-resolver.js'; diff --git a/src/interfaces/permissions/handlers/permissions-request.ts b/src/interfaces/permissions/handlers/permissions-request.ts index b37c9b269..9112a279d 100644 --- a/src/interfaces/permissions/handlers/permissions-request.ts +++ b/src/interfaces/permissions/handlers/permissions-request.ts @@ -4,36 +4,40 @@ import type { PermissionsRequestMessage } from '../types.js'; import { canonicalAuth } from '../../../core/auth.js'; import { MessageReply } from '../../../core/message-reply.js'; import { PermissionsRequest } from '../messages/permissions-request.js'; +import { DataStore, DidResolver, MessageStore } from '../../../index.js'; -export const handlePermissionsRequest: MethodHandler = async ({ - tenant, - message, - messageStore, - didResolver -}): Promise => { - const permissionRequest = await PermissionsRequest.parse(message as PermissionsRequestMessage); - const { author } = permissionRequest; +export class PermissionsRequestHandler implements MethodHandler { - if (tenant !== permissionRequest.grantedBy && tenant !== permissionRequest.grantedTo) { - return new MessageReply({ - status: { code: 400, detail: 'grantedBy or grantedTo must be the targeted message recipient' } - }); - } + constructor(private didResolver: DidResolver, private messageStore: MessageStore,private dataStore: DataStore) { } - await canonicalAuth(tenant, permissionRequest, didResolver); + public async handle({ + tenant, + message + }): Promise { + const permissionRequest = await PermissionsRequest.parse(message as PermissionsRequestMessage); + const { author } = permissionRequest; - if (author !== permissionRequest.grantedTo) { - throw new Error('grantee must be signer'); - } + if (tenant !== permissionRequest.grantedBy && tenant !== permissionRequest.grantedTo) { + return new MessageReply({ + status: { code: 400, detail: 'grantedBy or grantedTo must be the targeted message recipient' } + }); + } - const index = { - tenant, - author, - ... message.descriptor - }; - await messageStore.put(message, index); + await canonicalAuth(tenant, permissionRequest, this.didResolver); - return new MessageReply({ - status: { code: 202, detail: 'Accepted' } - }); -}; \ No newline at end of file + if (author !== permissionRequest.grantedTo) { + throw new Error('grantee must be signer'); + } + + const index = { + tenant, + author, + ... message.descriptor + }; + await this.messageStore.put(message, index); + + return new MessageReply({ + status: { code: 202, detail: 'Accepted' } + }); + }; +} \ No newline at end of file diff --git a/src/interfaces/permissions/permissions-interface.ts b/src/interfaces/permissions/permissions-interface.ts deleted file mode 100644 index 44338e86a..000000000 --- a/src/interfaces/permissions/permissions-interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { handlePermissionsRequest } from './handlers/permissions-request.js'; -import { PermissionsRequest } from './messages/permissions-request.js'; - -export const PermissionsInterface = { - methodHandlers : { 'PermissionsRequest': handlePermissionsRequest }, - messages : [ PermissionsRequest ] -}; \ No newline at end of file diff --git a/src/interfaces/protocols/handlers/protocols-configure.ts b/src/interfaces/protocols/handlers/protocols-configure.ts index 604b7542c..35e5c1537 100644 --- a/src/interfaces/protocols/handlers/protocols-configure.ts +++ b/src/interfaces/protocols/handlers/protocols-configure.ts @@ -4,80 +4,85 @@ import type { ProtocolsConfigureMessage } from '../types.js'; import { canonicalAuth } from '../../../core/auth.js'; import { MessageReply } from '../../../core/message-reply.js'; import { ProtocolsConfigure } from '../messages/protocols-configure.js'; +import { StorageController } from '../../../store/storage-controller.js'; +import { DataStore, DidResolver, MessageStore } from '../../../index.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js'; -export const handleProtocolsConfigure: MethodHandler = async ({ - tenant, - message, - messageStore, - didResolver, - dataStream -}): Promise => { - const incomingMessage = message as ProtocolsConfigureMessage; +export class ProtocolsConfigureHandler implements MethodHandler { - let protocolsConfigure: ProtocolsConfigure; - try { - protocolsConfigure = await ProtocolsConfigure.parse(incomingMessage); - } catch (e) { - return new MessageReply({ - status: { code: 400, detail: e.message } - }); - } + constructor(private didResolver: DidResolver, private messageStore: MessageStore,private dataStore: DataStore) { } - // authentication & authorization - try { - await canonicalAuth(tenant, protocolsConfigure, didResolver); - } catch (e) { - return new MessageReply({ - status: { code: 401, detail: e.message } - }); - } - - // attempt to get existing protocol - const query = { + public async handle({ tenant, - interface : DwnInterfaceName.Protocols, - method : DwnMethodName.Configure, - protocol : incomingMessage.descriptor.protocol - }; - const existingMessages = await messageStore.query(query) as ProtocolsConfigureMessage[]; + message, + dataStream + }): Promise { + const incomingMessage = message as ProtocolsConfigureMessage; - // find lexicographically the largest message, and if the incoming message is the largest - let newestMessage = await Message.getMessageWithLargestCid(existingMessages); - let incomingMessageIsNewest = false; - if (newestMessage === undefined || await Message.isCidLarger(incomingMessage, newestMessage)) { - incomingMessageIsNewest = true; - newestMessage = incomingMessage; - } + let protocolsConfigure: ProtocolsConfigure; + try { + protocolsConfigure = await ProtocolsConfigure.parse(incomingMessage); + } catch (e) { + return new MessageReply({ + status: { code: 400, detail: e.message } + }); + } + + // authentication & authorization + try { + await canonicalAuth(tenant, protocolsConfigure, this.didResolver); + } catch (e) { + return new MessageReply({ + status: { code: 401, detail: e.message } + }); + } - // write the incoming message to DB if incoming message is newest - let messageReply: MessageReply; - if (incomingMessageIsNewest) { - const { author } = protocolsConfigure; - const index = { + // attempt to get existing protocol + const query = { tenant, - author, - ... message.descriptor + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + protocol : incomingMessage.descriptor.protocol }; - await messageStore.put(message, index, dataStream); + const existingMessages = await this.messageStore.query(query) as ProtocolsConfigureMessage[]; - messageReply = new MessageReply({ - status: { code: 202, detail: 'Accepted' } - }); - } else { - messageReply = new MessageReply({ - status: { code: 409, detail: 'Conflict' } - }); - } + // find lexicographically the largest message, and if the incoming message is the largest + let newestMessage = await Message.getMessageWithLargestCid(existingMessages); + let incomingMessageIsNewest = false; + if (newestMessage === undefined || await Message.isCidLarger(incomingMessage, newestMessage)) { + incomingMessageIsNewest = true; + newestMessage = incomingMessage; + } + + // write the incoming message to DB if incoming message is newest + let messageReply: MessageReply; + if (incomingMessageIsNewest) { + const { author } = protocolsConfigure; + const indexes = { + tenant, + author, + ... message.descriptor + }; + await StorageController.put(this.messageStore, this.dataStore, incomingMessage, indexes, dataStream); - // delete all existing records that are smaller - for (const message of existingMessages) { - if (await Message.isCidLarger(newestMessage, message)) { - const cid = await Message.getCid(message); - await messageStore.delete(cid); + messageReply = new MessageReply({ + status: { code: 202, detail: 'Accepted' } + }); + } else { + messageReply = new MessageReply({ + status: { code: 409, detail: 'Conflict' } + }); } - } - return messageReply; -}; + // delete all existing records that are smaller + for (const message of existingMessages) { + if (await Message.isCidLarger(newestMessage, message)) { + const cid = await Message.getCid(message); + await this.messageStore.delete(cid); + } + } + + return messageReply; + }; +} \ No newline at end of file diff --git a/src/interfaces/protocols/handlers/protocols-query.ts b/src/interfaces/protocols/handlers/protocols-query.ts index 5cf0e2d15..39e716a39 100644 --- a/src/interfaces/protocols/handlers/protocols-query.ts +++ b/src/interfaces/protocols/handlers/protocols-query.ts @@ -5,45 +5,50 @@ import { canonicalAuth } from '../../../core/auth.js'; import { MessageReply } from '../../../core/message-reply.js'; import { ProtocolsQuery } from '../messages/protocols-query.js'; import { removeUndefinedProperties } from '../../../utils/object.js'; + +import { DataStore, DidResolver, MessageStore } from '../../../index.js'; import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js'; -export const handleProtocolsQuery: MethodHandler = async ({ - tenant, - message, - messageStore, - didResolver -}): Promise => { - const incomingMessage = message as ProtocolsQueryMessage; - - let protocolsQuery: ProtocolsQuery; - try { - protocolsQuery = await ProtocolsQuery.parse(incomingMessage); - } catch (e) { - return new MessageReply({ - status: { code: 400, detail: e.message } - }); - } +export class ProtocolsQueryHandler implements MethodHandler { - try { - await canonicalAuth(tenant, protocolsQuery, didResolver); - } catch (e) { - return new MessageReply({ - status: { code: 401, detail: e.message } - }); - } + constructor(private didResolver: DidResolver, private messageStore: MessageStore,private dataStore: DataStore) { } - const query = { + public async handle({ tenant, - interface : DwnInterfaceName.Protocols, - method : DwnMethodName.Configure, - ...incomingMessage.descriptor.filter - }; - removeUndefinedProperties(query); + message + }): Promise { + const incomingMessage = message as ProtocolsQueryMessage; - const entries = await messageStore.query(query); + let protocolsQuery: ProtocolsQuery; + try { + protocolsQuery = await ProtocolsQuery.parse(incomingMessage); + } catch (e) { + return new MessageReply({ + status: { code: 400, detail: e.message } + }); + } - return new MessageReply({ - status: { code: 200, detail: 'OK' }, - entries - }); -}; + try { + await canonicalAuth(tenant, protocolsQuery, this.didResolver); + } catch (e) { + return new MessageReply({ + status: { code: 401, detail: e.message } + }); + } + + const query = { + tenant, + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + ...incomingMessage.descriptor.filter + }; + removeUndefinedProperties(query); + + const entries = await this.messageStore.query(query); + + return new MessageReply({ + status: { code: 200, detail: 'OK' }, + entries + }); + }; +} diff --git a/src/interfaces/protocols/protocols-interface.ts b/src/interfaces/protocols/protocols-interface.ts deleted file mode 100644 index 15b3ff9a4..000000000 --- a/src/interfaces/protocols/protocols-interface.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { handleProtocolsConfigure } from './handlers/protocols-configure.js'; -import { handleProtocolsQuery } from './handlers/protocols-query.js'; -import { ProtocolsConfigure } from './messages/protocols-configure.js'; -import { ProtocolsQuery } from './messages/protocols-query.js'; -import { DwnInterfaceName, DwnMethodName } from '../../core/message.js'; - -export const ProtocolsInterface = { - methodHandlers: { - [DwnInterfaceName.Protocols + DwnMethodName.Configure] : handleProtocolsConfigure, - [DwnInterfaceName.Protocols + DwnMethodName.Query] : handleProtocolsQuery - }, - messages: [ - ProtocolsConfigure, - ProtocolsQuery - ] -}; \ No newline at end of file diff --git a/src/interfaces/records/handlers/records-delete.ts b/src/interfaces/records/handlers/records-delete.ts index b04ac32ec..1d60feeb2 100644 --- a/src/interfaces/records/handlers/records-delete.ts +++ b/src/interfaces/records/handlers/records-delete.ts @@ -8,75 +8,79 @@ import { MessageReply } from '../../../core/message-reply.js'; import { RecordsDelete } from '../messages/records-delete.js'; import { RecordsWrite } from '../messages/records-write.js'; import { TimestampedMessage } from '../../../core/types.js'; +import { DataStore, DidResolver, MessageStore } from '../../../index.js'; -export const handleRecordsDelete: MethodHandler = async ({ - tenant, - message, - messageStore, - didResolver -}): Promise => { - const incomingMessage = message as RecordsDeleteMessage; - - let recordsDelete: RecordsDelete; - try { - recordsDelete = await RecordsDelete.parse(incomingMessage); - } catch (e) { - return new MessageReply({ - status: { code: 400, detail: e.message } - }); - } - - // authentication & authorization - try { - await authenticate(message.authorization, didResolver); - await recordsDelete.authorize(tenant); - } catch (e) { - return new MessageReply({ - status: { code: 401, detail: e.message } - }); - } - - // get existing records matching the `recordId` - const query = { +export class RecordsDeleteHandler implements MethodHandler { + + constructor(private didResolver: DidResolver, private messageStore: MessageStore,private dataStore: DataStore) { } + + public async handle({ tenant, - interface : DwnInterfaceName.Records, - recordId : incomingMessage.descriptor.recordId + message + }): Promise { + const incomingMessage = message as RecordsDeleteMessage; + + let recordsDelete: RecordsDelete; + try { + recordsDelete = await RecordsDelete.parse(incomingMessage); + } catch (e) { + return new MessageReply({ + status: { code: 400, detail: e.message } + }); + } + + // authentication & authorization + try { + await authenticate(message.authorization, this.didResolver); + await recordsDelete.authorize(tenant); + } catch (e) { + return new MessageReply({ + status: { code: 401, detail: e.message } + }); + } + + // get existing records matching the `recordId` + const query = { + tenant, + interface : DwnInterfaceName.Records, + recordId : incomingMessage.descriptor.recordId + }; + const existingMessages = await this.messageStore.query(query) as TimestampedMessage[]; + + // find which message is the newest, and if the incoming message is the newest + const newestExistingMessage = await RecordsWrite.getNewestMessage(existingMessages); + let incomingMessageIsNewest = false; + let newestMessage; + // if incoming message is newest + if (newestExistingMessage === undefined || await RecordsWrite.isNewer(incomingMessage, newestExistingMessage)) { + incomingMessageIsNewest = true; + newestMessage = incomingMessage; + } else { // existing message is the same age or newer than the incoming message + newestMessage = newestExistingMessage; + } + + // write the incoming message to DB if incoming message is newest + let messageReply: MessageReply; + if (incomingMessageIsNewest) { + const indexes = await constructIndexes(tenant, recordsDelete); + + await this.messageStore.put(incomingMessage, indexes); + + messageReply = new MessageReply({ + status: { code: 202, detail: 'Accepted' } + }); + } else { + messageReply = new MessageReply({ + status: { code: 409, detail: 'Conflict' } + }); + } + + // delete all existing messages that are not newest, except for the initial write + await deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, this.messageStore); + + return messageReply; }; - const existingMessages = await messageStore.query(query) as TimestampedMessage[]; - - // find which message is the newest, and if the incoming message is the newest - const newestExistingMessage = await RecordsWrite.getNewestMessage(existingMessages); - let incomingMessageIsNewest = false; - let newestMessage; - // if incoming message is newest - if (newestExistingMessage === undefined || await RecordsWrite.isNewer(incomingMessage, newestExistingMessage)) { - incomingMessageIsNewest = true; - newestMessage = incomingMessage; - } else { // existing message is the same age or newer than the incoming message - newestMessage = newestExistingMessage; - } - - // write the incoming message to DB if incoming message is newest - let messageReply: MessageReply; - if (incomingMessageIsNewest) { - const indexes = await constructIndexes(tenant, recordsDelete); - - await messageStore.put(incomingMessage, indexes); - - messageReply = new MessageReply({ - status: { code: 202, detail: 'Accepted' } - }); - } else { - messageReply = new MessageReply({ - status: { code: 409, detail: 'Conflict' } - }); - } - - // delete all existing messages that are not newest, except for the initial write - await deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, messageStore); - - return messageReply; -}; +} export async function constructIndexes(tenant: string, recordsDelete: RecordsDelete): Promise<{ [key: string]: string }> { const message = recordsDelete.message; diff --git a/src/interfaces/records/handlers/records-query.ts b/src/interfaces/records/handlers/records-query.ts index fee572472..a3cd555b9 100644 --- a/src/interfaces/records/handlers/records-query.ts +++ b/src/interfaces/records/handlers/records-query.ts @@ -5,156 +5,159 @@ import { authenticate } from '../../../core/auth.js'; import { BaseMessage } from '../../../core/types.js'; import { lexicographicalCompare } from '../../../utils/string.js'; import { MessageReply } from '../../../core/message-reply.js'; -import { MessageStore } from '../../../store/message-store.js'; +import { StorageController } from '../../../store/storage-controller.js'; +import { DataStore, DidResolver, MessageStore } from '../../../index.js'; import { DateSort, RecordsQuery } from '../messages/records-query.js'; import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js'; -export const handleRecordsQuery: MethodHandler = async ({ - tenant, - message, - messageStore, - didResolver -}): Promise => { - let recordsQuery: RecordsQuery; - try { - recordsQuery = await RecordsQuery.parse(message as RecordsQueryMessage); - } catch (e) { - return new MessageReply({ - status: { code: 400, detail: e.message } - }); - } +export class RecordsQueryHandler implements MethodHandler { + + constructor(private didResolver: DidResolver, private messageStore: MessageStore,private dataStore: DataStore) { } + + public async handle({ + tenant, + message + }): Promise { + let recordsQuery: RecordsQuery; + try { + recordsQuery = await RecordsQuery.parse(message as RecordsQueryMessage); + } catch (e) { + return new MessageReply({ + status: { code: 400, detail: e.message } + }); + } + + try { + await authenticate(message.authorization, this.didResolver); + await recordsQuery.authorize(tenant); + } catch (e) { + return new MessageReply({ + status: { code: 401, detail: e.message } + }); + } + + let records: BaseMessage[]; + if (recordsQuery.author === tenant) { + records = await this.fetchRecordsAsOwner(tenant, recordsQuery); + } else { + records = await this.fetchRecordsAsNonOwner(tenant, recordsQuery); + } + + // sort if `dataSort` is specified + if (recordsQuery.message.descriptor.dateSort) { + records = await sortRecords(records, recordsQuery.message.descriptor.dateSort); + } + + // strip away `authorization` property for each record before responding + const entries = []; + for (const record of records) { + const recordDuplicate = { ...record }; + delete recordDuplicate.authorization; + entries.push(recordDuplicate); + } - try { - await authenticate(message.authorization, didResolver); - await recordsQuery.authorize(tenant); - } catch (e) { return new MessageReply({ - status: { code: 401, detail: e.message } + status: { code: 200, detail: 'OK' }, + entries }); } - let records: BaseMessage[]; - if (recordsQuery.author === tenant) { - records = await fetchRecordsAsOwner(tenant, recordsQuery, messageStore); - } else { - records = await fetchRecordsAsNonOwner(tenant, recordsQuery, messageStore); + /** + * Fetches the records as the owner of the DWN with no additional filtering. + */ + private async fetchRecordsAsOwner(tenant: string, recordsQuery: RecordsQuery): Promise { + // fetch all published records matching the query + const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); + const completeExactCriteria = { + tenant, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + isLatestBaseState : 'true', + ...exactCriteria + }; + + const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); + const records = await StorageController.query(this.messageStore, this.dataStore, completeExactCriteria, rangeCriteria); + return records; } - // sort if `dataSort` is specified - if (recordsQuery.message.descriptor.dateSort) { - records = await sortRecords(records, recordsQuery.message.descriptor.dateSort); + /** + * Fetches the records as a non-owner, return only: + * 1. published records; and + * 2. unpublished records intended for the requester (where `recipient` is the requester) + */ + private async fetchRecordsAsNonOwner(tenant: string, recordsQuery: RecordsQuery) + : Promise { + const publishedRecords = await this.fetchPublishedRecords(tenant, recordsQuery); + const unpublishedRecordsForRequester = await this.fetchUnpublishedRecordsForRequester(tenant, recordsQuery); + const unpublishedRecordsByRequester = await this.fetchUnpublishedRecordsByRequester(tenant, recordsQuery); + const records = [...publishedRecords, ...unpublishedRecordsForRequester, ...unpublishedRecordsByRequester]; + return records; } - // strip away `authorization` property for each record before responding - const entries = []; - for (const record of records) { - const recordDuplicate = { ...record }; - delete recordDuplicate.authorization; - entries.push(recordDuplicate); + /** + * Fetches only published records. + */ + private async fetchPublishedRecords(tenant: string, recordsQuery: RecordsQuery): Promise { + // fetch all published records matching the query + const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); + const completeExactCriteria = { + tenant, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + published : 'true', + isLatestBaseState : 'true', + ...exactCriteria + }; + + const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); + const publishedRecords = await StorageController.query(this.messageStore, this.dataStore, completeExactCriteria, rangeCriteria); + return publishedRecords; } - return new MessageReply({ - status: { code: 200, detail: 'OK' }, - entries - }); -}; -/** - * Fetches the records as the owner of the DWN with no additional filtering. - */ -async function fetchRecordsAsOwner(tenant: string, recordsQuery: RecordsQuery, messageStore: MessageStore): Promise { - // fetch all published records matching the query - const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); - const completeExactCriteria = { - tenant, - interface : DwnInterfaceName.Records, - method : DwnMethodName.Write, - isLatestBaseState : 'true', - ...exactCriteria - }; - - const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); - const records = await messageStore.query(completeExactCriteria, rangeCriteria); - return records; -} - -/** - * Fetches the records as a non-owner, return only: - * 1. published records; and - * 2. unpublished records intended for the requester (where `recipient` is the requester) - */ -async function fetchRecordsAsNonOwner(tenant: string, recordsQuery: RecordsQuery, messageStore: MessageStore) - : Promise { - const publishedRecords = await fetchPublishedRecords(tenant, recordsQuery, messageStore); - const unpublishedRecordsForRequester = await fetchUnpublishedRecordsForRequester(tenant, recordsQuery, messageStore); - const unpublishedRecordsByRequester = await fetchUnpublishedRecordsByRequester(tenant, recordsQuery, messageStore); - const records = [...publishedRecords, ...unpublishedRecordsForRequester, ...unpublishedRecordsByRequester]; - return records; -} - -/** - * Fetches only published records. - */ -async function fetchPublishedRecords(tenant: string, recordsQuery: RecordsQuery, messageStore: MessageStore): Promise { - // fetch all published records matching the query - const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); - const completeExactCriteria = { - tenant, - interface : DwnInterfaceName.Records, - method : DwnMethodName.Write, - published : 'true', - isLatestBaseState : 'true', - ...exactCriteria - }; - - const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); - const publishedRecords = await messageStore.query(completeExactCriteria, rangeCriteria); - return publishedRecords; -} - -/** - * Fetches only unpublished records that are intended for the requester (where `recipient` is the requester). - */ -async function fetchUnpublishedRecordsForRequester(tenant: string, recordsQuery: RecordsQuery, messageStore: MessageStore) - : Promise { + /** + * Fetches only unpublished records that are intended for the requester (where `recipient` is the requester). + */ + private async fetchUnpublishedRecordsForRequester(tenant: string, recordsQuery: RecordsQuery): Promise { // include records where recipient is requester - const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); - const completeExactCriteria = { - tenant, - interface : DwnInterfaceName.Records, - method : DwnMethodName.Write, - recipient : recordsQuery.author, - isLatestBaseState : 'true', - published : 'false', - ...exactCriteria - }; - - const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); - const unpublishedRecordsForRequester = await messageStore.query(completeExactCriteria, rangeCriteria); - return unpublishedRecordsForRequester; -} + const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); + const completeExactCriteria = { + tenant, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + recipient : recordsQuery.author, + isLatestBaseState : 'true', + published : 'false', + ...exactCriteria + }; + + const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); + const unpublishedRecordsForRequester = await StorageController.query(this.messageStore, this.dataStore, completeExactCriteria, rangeCriteria); + return unpublishedRecordsForRequester; + } -/** - * Fetches only unpublished records that are authored by the requester. - */ -async function fetchUnpublishedRecordsByRequester(tenant: string, recordsQuery: RecordsQuery, messageStore: MessageStore) - : Promise { - // include records where recipient is requester - const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); - const completeExactCriteria = { - tenant, - author : recordsQuery.author, - interface : DwnInterfaceName.Records, - method : DwnMethodName.Write, - isLatestBaseState : 'true', - published : 'false', - ...exactCriteria - }; - - const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); - - const unpublishedRecordsForRequester = await messageStore.query(completeExactCriteria, rangeCriteria); - return unpublishedRecordsForRequester; + /** + * Fetches only unpublished records that are authored by the requester. + */ + private async fetchUnpublishedRecordsByRequester(tenant: string, recordsQuery: RecordsQuery): Promise { + // include records where recipient is requester + const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter); + const completeExactCriteria = { + tenant, + author : recordsQuery.author, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + isLatestBaseState : 'true', + published : 'false', + ...exactCriteria + }; + + const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter); + + const unpublishedRecordsForRequester = await StorageController.query(this.messageStore, this.dataStore, completeExactCriteria, rangeCriteria); + return unpublishedRecordsForRequester; + } } /** diff --git a/src/interfaces/records/handlers/records-write.ts b/src/interfaces/records/handlers/records-write.ts index ae6dfe2a2..cd706a2f7 100644 --- a/src/interfaces/records/handlers/records-write.ts +++ b/src/interfaces/records/handlers/records-write.ts @@ -7,104 +7,110 @@ import { DwnErrorCode } from '../../../core/dwn-error.js'; import { DwnInterfaceName } from '../../../core/message.js'; import { MessageReply } from '../../../core/message-reply.js'; import { RecordsWrite } from '../messages/records-write.js'; +import { StorageController } from '../../../store/storage-controller.js'; import { TimestampedMessage } from '../../../core/types.js'; -export const handleRecordsWrite: MethodHandler = async ({ - tenant, - message, - messageStore, - didResolver, - dataStream -}): Promise => { - const incomingMessage = message as RecordsWriteMessage; - - let recordsWrite: RecordsWrite; - try { - recordsWrite = await RecordsWrite.parse(incomingMessage); - } catch (e) { - return new MessageReply({ - status: { code: 400, detail: e.message } - }); - } +import { DataStore, DidResolver, MessageStore } from '../../../index.js'; - // authentication & authorization - try { - await authenticate(message.authorization, didResolver); - await recordsWrite.authorize(tenant, messageStore); - } catch (e) { - return new MessageReply({ - status: { code: 401, detail: e.message } - }); - } +export class RecordsWriteHandler implements MethodHandler { - // get existing messages matching the `recordId` - const query = { + constructor(private didResolver: DidResolver, private messageStore: MessageStore,private dataStore: DataStore) { } + + public async handle({ tenant, - interface : DwnInterfaceName.Records, - recordId : incomingMessage.recordId - }; - const existingMessages = await messageStore.query(query) as TimestampedMessage[]; + message, + dataStream + }): Promise { + const incomingMessage = message as RecordsWriteMessage; - // if the incoming write is not the initial write, then it must not modify any immutable properties defined by the initial write - const newMessageIsInitialWrite = await recordsWrite.isInitialWrite(); - if (!newMessageIsInitialWrite) { + let recordsWrite: RecordsWrite; try { - const initialWrite = RecordsWrite.getInitialWrite(existingMessages); - RecordsWrite.verifyEqualityOfImmutableProperties(initialWrite, incomingMessage); + recordsWrite = await RecordsWrite.parse(incomingMessage); } catch (e) { return new MessageReply({ status: { code: 400, detail: e.message } }); } - } - - // find which message is the newest, and if the incoming message is the newest - const newestExistingMessage = await RecordsWrite.getNewestMessage(existingMessages); - - let incomingMessageIsNewest = false; - let newestMessage; - // if incoming message is newest - if (newestExistingMessage === undefined || await RecordsWrite.isNewer(incomingMessage, newestExistingMessage)) { - incomingMessageIsNewest = true; - newestMessage = incomingMessage; - } else { // existing message is the same age or newer than the incoming message - newestMessage = newestExistingMessage; - } - - // write the incoming message to DB if incoming message is newest - let messageReply: MessageReply; - if (incomingMessageIsNewest) { - const isLatestBaseState = true; - const indexes = await constructRecordsWriteIndexes(tenant, recordsWrite, isLatestBaseState); + // authentication & authorization try { - await messageStore.put(incomingMessage, indexes, dataStream); - } catch (error) { - if (error.code === DwnErrorCode.MessageStoreDataCidMismatch || - error.code === DwnErrorCode.MessageStoreDataNotFound) { + await authenticate(message.authorization, this.didResolver); + await recordsWrite.authorize(tenant, this.messageStore); + } catch (e) { + return new MessageReply({ + status: { code: 401, detail: e.message } + }); + } + + // get existing messages matching the `recordId` + const query = { + tenant, + interface : DwnInterfaceName.Records, + recordId : incomingMessage.recordId + }; + const existingMessages = await this.messageStore.query(query) as TimestampedMessage[]; + + // if the incoming write is not the initial write, then it must not modify any immutable properties defined by the initial write + const newMessageIsInitialWrite = await recordsWrite.isInitialWrite(); + if (!newMessageIsInitialWrite) { + try { + const initialWrite = RecordsWrite.getInitialWrite(existingMessages); + RecordsWrite.verifyEqualityOfImmutableProperties(initialWrite, incomingMessage); + } catch (e) { return new MessageReply({ - status: { code: 400, detail: error.message } + status: { code: 400, detail: e.message } }); } + } - // else throw - throw error; + // find which message is the newest, and if the incoming message is the newest + const newestExistingMessage = await RecordsWrite.getNewestMessage(existingMessages); + + let incomingMessageIsNewest = false; + let newestMessage; + // if incoming message is newest + if (newestExistingMessage === undefined || await RecordsWrite.isNewer(incomingMessage, newestExistingMessage)) { + incomingMessageIsNewest = true; + newestMessage = incomingMessage; + } else { // existing message is the same age or newer than the incoming message + newestMessage = newestExistingMessage; } - messageReply = new MessageReply({ - status: { code: 202, detail: 'Accepted' } - }); - } else { - messageReply = new MessageReply({ - status: { code: 409, detail: 'Conflict' } - }); - } + // write the incoming message to DB if incoming message is newest + let messageReply: MessageReply; + if (incomingMessageIsNewest) { + const isLatestBaseState = true; + const indexes = await constructRecordsWriteIndexes(tenant, recordsWrite, isLatestBaseState); - // delete all existing messages that are not newest, except for the initial write - await deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, messageStore); + try { + await StorageController.put(this.messageStore, this.dataStore, incomingMessage, indexes, dataStream); + } catch (error) { + if (error.code === DwnErrorCode.MessageStoreDataCidMismatch || + error.code === DwnErrorCode.MessageStoreDataNotFound) { + return new MessageReply({ + status: { code: 400, detail: error.message } + }); + } - return messageReply; -}; + // else throw + throw error; + } + + messageReply = new MessageReply({ + status: { code: 202, detail: 'Accepted' } + }); + } else { + messageReply = new MessageReply({ + status: { code: 409, detail: 'Conflict' } + }); + } + + // delete all existing messages that are not newest, except for the initial write + await deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, this.messageStore); + + return messageReply; + }; +} export async function constructRecordsWriteIndexes( tenant: string, diff --git a/src/interfaces/records/records-interface.ts b/src/interfaces/records/records-interface.ts index dada603a0..a0c48b2d5 100644 --- a/src/interfaces/records/records-interface.ts +++ b/src/interfaces/records/records-interface.ts @@ -1,27 +1,10 @@ -import { handleRecordsDelete } from './handlers/records-delete.js'; -import { handleRecordsQuery } from './handlers/records-query.js'; import { MessageStore } from '../../store/message-store.js'; -import { RecordsDelete } from './messages/records-delete.js'; -import { RecordsQuery } from './messages/records-query.js'; import { RecordsWrite } from './messages/records-write.js'; import { RecordsWriteMessage } from '../../interfaces/records/types.js'; import { TimestampedMessage } from '../../core/types.js'; -import { constructRecordsWriteIndexes, handleRecordsWrite } from './handlers/records-write.js'; -import { DwnInterfaceName, DwnMethodName, Message } from '../../core/message.js'; - -export const RecordsInterface = { - methodHandlers: { - [DwnInterfaceName.Records + DwnMethodName.Query] : handleRecordsDelete, - [DwnInterfaceName.Records + DwnMethodName.Query] : handleRecordsQuery, - [DwnInterfaceName.Records + DwnMethodName.Write] : handleRecordsWrite - }, - messages: [ - RecordsDelete, - RecordsQuery, - RecordsWrite - ] -}; +import { constructRecordsWriteIndexes } from './handlers/records-write.js'; +import { Message } from '../../core/message.js'; /** * Deletes all messages in `existingMessages` that are older than the `comparedToMessage` in the given tenant, diff --git a/src/interfaces/types.ts b/src/interfaces/types.ts index 9615648fc..233ed57a0 100644 --- a/src/interfaces/types.ts +++ b/src/interfaces/types.ts @@ -1,14 +1,18 @@ import type { BaseMessage } from '../core/types.js'; import type { MessageReply } from '../core/message-reply.js'; -import type { MessageStore } from '../store/message-store.js'; -import { DidResolver } from '../did/did-resolver.js'; import { Readable } from 'readable-stream'; -export type MethodHandler = (input: { - tenant: string; - message: BaseMessage; - dataStream?: Readable - messageStore: MessageStore; - didResolver: DidResolver -}) => Promise; +/** + * Interface that defines a message handler of a specific method. + */ +export interface MethodHandler { + /** + * Handles the given message and returns a `MessageReply` response. + */ + handle(input: { + tenant: string; + message: BaseMessage; + dataStream?: Readable + }): Promise; +} \ No newline at end of file diff --git a/src/store/blockstore-level.ts b/src/store/blockstore-level.ts index b4306591c..0df379566 100644 --- a/src/store/blockstore-level.ts +++ b/src/store/blockstore-level.ts @@ -58,7 +58,7 @@ export class BlockstoreLevel implements Blockstore { return this.db.put(key.toString(), val); } - async get(key: CID, _options?: Options): Promise { + async get(key: CID, _options?: Options): Promise { try { const val = await this.db.get(key.toString()); return val; diff --git a/src/store/data-store-level.ts b/src/store/data-store-level.ts new file mode 100644 index 000000000..a0a4d45da --- /dev/null +++ b/src/store/data-store-level.ts @@ -0,0 +1,81 @@ +import { BlockstoreLevel } from './blockstore-level.js'; +import { CID } from 'multiformats/cid'; +import { DataStore } from './data-store.js'; +import { exporter } from 'ipfs-unixfs-exporter'; +import { importer } from 'ipfs-unixfs-importer'; +import { Readable } from 'readable-stream'; + +/** + * A simple implementation of {@link MessageStore} that works in both the browser and server-side. + * Leverages LevelDB under the hood. + */ +export class DataStoreLevel implements DataStore { + blockstore: BlockstoreLevel; + + constructor(blockstoreLocation: string = 'DATASTORE') { + this.blockstore = new BlockstoreLevel(blockstoreLocation); + } + + public async open(): Promise { + await this.blockstore.open(); + } + + async close(): Promise { + await this.blockstore.close(); + } + + async put(tenant: string, recordId: string, dataStream: Readable): Promise { + const asyncDataBlocks = importer([{ content: dataStream }], this.blockstore, { cidVersion: 1 }); + + // NOTE: the last block contains the root CID + let block; + for await (block of asyncDataBlocks) { ; } + + // MUST verify that the CID of the actual data matches with the given `dataCid` + // if data CID is wrong, delete the data we just stored + const dataCid = block.cid.toString(); + return dataCid; + } + + public async get(tenant: string, recordId: string, dataCid: string): Promise { + const cid = CID.parse(dataCid); + const bytes = await this.blockstore.get(cid); + + if (!bytes) { + return undefined; + } + + // data is chunked into dag-pb unixfs blocks. re-inflate the chunks. + const dataDagRoot = await exporter(dataCid, this.blockstore); + const dataBytes = new Uint8Array(dataDagRoot.size); + let offset = 0; + + for await (const chunk of dataDagRoot.content()) { + dataBytes.set(chunk, offset); + offset += chunk.length; + } + + return dataBytes; + } + + public async has(tenant: string, recordId: string, dataCid: string): Promise { + const cid = CID.parse(dataCid); + const rootBlockBytes = await this.blockstore.get(cid); + + return (rootBlockBytes !== undefined); + } + + public async delete(tenant: string, recordId: string, dataCid: string): Promise { + // TODO: Implement data deletion in Records - https://github.com/TBD54566975/dwn-sdk-js/issues/84 + const cid = CID.parse(dataCid); + await this.blockstore.delete(cid); + return; + } + + /** + * Deletes everything in the store. Mainly used in tests. + */ + public async clear(): Promise { + await this.blockstore.clear(); + } +} diff --git a/src/store/data-store.ts b/src/store/data-store.ts new file mode 100644 index 000000000..8738ab2d3 --- /dev/null +++ b/src/store/data-store.ts @@ -0,0 +1,43 @@ +import type { Readable } from 'readable-stream'; + +/** + * The interface that defines how to store and fetch data associated with a message + */ +export interface DataStore { + /** + * Opens a connection to the underlying store. + */ + open(): Promise; + + /** + * Closes the connection to the underlying store. + */ + close(): Promise; + + /** + * Puts the given data in store. + * @param logicalId This may be the ID of a record, or the ID of a protocol definition etc. + * @returns The CID of the data stored. + */ + put(tenant: string, logicalId: string, dataStream: Readable): Promise; + + /** + * Fetches the specified data. + * TODO: #205 - https://github.com/TBD54566975/dwn-sdk-js/issues/205 + * change return type from Uint8Array to a readable stream + * @param logicalId This may be the ID of a record, or the ID of a protocol definition etc. + */ + get(tenant: string, logicalId: string, dataCid: string): Promise; + + /** + * Checks to see if the store has the specified data. + * @param logicalId This may be the ID of a record, or the ID of a protocol definition etc. + */ + has(tenant: string, logicalId: string, dataCid: string): Promise; + + /** + * Deletes the specified data; + * @param logicalId This may be the ID of a record, or the ID of a protocol definition etc. + */ + delete(tenant: string, logicalId: string, dataCid: string): Promise; +} \ No newline at end of file diff --git a/src/store/message-store-level.ts b/src/store/message-store-level.ts index 134b831fc..c6bdb48e2 100644 --- a/src/store/message-store-level.ts +++ b/src/store/message-store-level.ts @@ -1,5 +1,5 @@ +import type { BaseMessage } from '../core/types.js'; import type { MessageStore } from './message-store.js'; -import type { BaseMessage, DataReferencingMessage } from '../core/types.js'; import * as block from 'multiformats/block'; import * as cbor from '@ipld/dag-cbor'; @@ -8,22 +8,18 @@ import searchIndex from 'search-index'; import { BlockstoreLevel } from './blockstore-level.js'; import { CID } from 'multiformats/cid'; -import { Encoder } from '../utils/encoder.js'; -import { exporter } from 'ipfs-unixfs-exporter'; -import { importer } from 'ipfs-unixfs-importer'; import { RangeCriterion } from '../interfaces/records/types.js'; -import { Readable } from 'readable-stream'; import { sha256 } from 'multiformats/hashes/sha2'; -import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; - /** * A simple implementation of {@link MessageStore} that works in both the browser and server-side. * Leverages LevelDB under the hood. */ export class MessageStoreLevel implements MessageStore { config: MessageStoreLevelConfig; - db: BlockstoreLevel; + + public readonly blockstore: BlockstoreLevel; + // levelDB doesn't natively provide the querying capabilities needed for DWN, // to accommodate, we're leveraging a level-backed inverted index. index: Awaited>; // type `SearchIndex` is not exported. So we construct it indirectly from function return type @@ -42,15 +38,11 @@ export class MessageStoreLevel implements MessageStore { ...config }; - this.db = new BlockstoreLevel(this.config.blockstoreLocation); + this.blockstore = new BlockstoreLevel(this.config.blockstoreLocation); } async open(): Promise { - if (!this.db) { - this.db = new BlockstoreLevel(this.config.blockstoreLocation); - } - - await this.db.open(); + await this.blockstore.open(); // calling `searchIndex()` twice without closing its DB causes the process to hang (ie. calling this method consecutively), // so check to see if the index has already been "opened" before opening it again. @@ -60,52 +52,28 @@ export class MessageStoreLevel implements MessageStore { } async close(): Promise { - await this.db.close(); + await this.blockstore.close(); await this.index.INDEX.STORE.close(); // MUST close index-search DB, else `searchIndex()` triggered in a different instance will hang indefinitely } - async get(cidString: string): Promise { + async get(cidString: string): Promise { const cid = CID.parse(cidString); - const bytes = await this.db.get(cid); + const bytes = await this.blockstore.get(cid); if (!bytes) { - return; + return undefined; } const decodedBlock = await block.decode({ bytes, codec: cbor, hasher: sha256 }); const messageJson = decodedBlock.value as BaseMessage; - - if (!messageJson.descriptor['dataCid']) { - return messageJson; - } - - // TODO: #219 (https://github.com/TBD54566975/dwn-sdk-js/issues/219) - // temporary placeholder for keeping status-quo of returning data in `encodedData` - // once #219 is implemented, `encodedData` will likely not exist directly as part of the returned message here - const dataReferencingMessage = decodedBlock.value as DataReferencingMessage; - dataReferencingMessage.encodedData = await this.getDataAsEncodedString(dataReferencingMessage.descriptor.dataCid); - return messageJson; } - private async getDataAsEncodedString(dataCid: string): Promise { - const cid = CID.parse(dataCid); - - // data is chunked into dag-pb unixfs blocks. re-inflate the chunks. - const dataDagRoot = await exporter(cid, this.db); - const dataBytes = new Uint8Array(dataDagRoot.size); - let offset = 0; - - for await (const chunk of dataDagRoot.content()) { - dataBytes.set(chunk, offset); - offset += chunk.length; - } - - return Encoder.bytesToBase64Url(dataBytes); - } - - async query(exactCriteria: { [key: string]: string }, rangeCriteria?: { [key: string]: RangeCriterion }): Promise { + async query( + exactCriteria: { [key: string]: string }, + rangeCriteria?: { [key: string]: RangeCriterion } + ): Promise { const messages: BaseMessage[] = []; // parse criteria into a query that is compatible with the indexing DB (search-index) we're using @@ -125,54 +93,16 @@ export class MessageStoreLevel implements MessageStore { async delete(cidString: string): Promise { // TODO: Implement data deletion in Records - https://github.com/TBD54566975/dwn-sdk-js/issues/84 const cid = CID.parse(cidString); - await this.db.delete(cid); + await this.blockstore.delete(cid); await this.index.DELETE(cidString); return; } - async put(message: BaseMessage, indexes: { [key: string]: string }, dataStream?: Readable): Promise { + async put(message: BaseMessage, indexes: { [key: string]: string }): Promise { const encodedMessageBlock = await block.encode({ value: message, codec: cbor, hasher: sha256 }); - await this.db.put(encodedMessageBlock.cid, encodedMessageBlock.bytes); - - // if `dataCid` is given, it means there is corresponding data associated with this message - // but NOTE: it is possible that a data stream is not given in such case, for instance, - // a subsequent RecordsWrite that changes the `published` property, but the data hasn't changed, - // in this case requiring re-uploading of the data is extremely inefficient so we take care allow omission of data stream - if (message.descriptor.dataCid !== undefined) { - if (dataStream === undefined) { - // the message implies that the data is already in the DB, so we check to make sure the data already exist - // TODO: #218 - Use tenant + record scoped IDs - https://github.com/TBD54566975/dwn-sdk-js/issues/218 - const dataCid = CID.parse(message.descriptor.dataCid); - const rootBlockByte = await this.db.get(dataCid); - - if (rootBlockByte === undefined) { - throw new DwnError( - DwnErrorCode.MessageStoreDataNotFound, - `data with dataCid ${message.descriptor.dataCid} not found in store` - ); - } - } else { - const asyncDataBlocks = importer([{ content: dataStream }], this.db, { cidVersion: 1 }); - - // NOTE: the last block contains the root CID - let block; - for await (block of asyncDataBlocks) { ; } - - // MUST verify that the CID of the actual data matches with the given `dataCid` - // if data CID is wrong, delete the data we just stored - const actualDataCid = block.cid.toString(); - if (message.descriptor.dataCid !== actualDataCid) { - // there is an opportunity to improve here: handle the edge cae of if the delete fails... - await this.db.delete(block.cid); - throw new DwnError( - DwnErrorCode.MessageStoreDataCidMismatch, - `actual data CID ${actualDataCid} does not match dataCid in descriptor: ${message.descriptor.dataCid}` - ); - } - } - } + await this.blockstore.put(encodedMessageBlock.cid, encodedMessageBlock.bytes); // TODO: #218 - Use tenant + record scoped IDs - https://github.com/TBD54566975/dwn-sdk-js/issues/218 const encodedMessageBlockCid = encodedMessageBlock.cid.toString(); @@ -193,7 +123,7 @@ export class MessageStoreLevel implements MessageStore { * deletes everything in the underlying datastore and indices. */ async clear(): Promise { - await this.db.clear(); + await this.blockstore.clear(); await this.index.FLUSH(); } diff --git a/src/store/message-store.ts b/src/store/message-store.ts index fa6fec094..176219274 100644 --- a/src/store/message-store.ts +++ b/src/store/message-store.ts @@ -1,6 +1,5 @@ import type { BaseMessage } from '../core/types.js'; import type { RangeCriterion } from '../interfaces/records/types.js'; -import type { Readable } from 'readable-stream'; export interface MessageStore { /** @@ -16,18 +15,14 @@ export interface MessageStore { /** * adds a message to the underlying store. Uses the message's cid as the key * @param indexes indexes (key-value pairs) to be included as part of this put operation - * @throws {DwnError} with `DwnErrorCode.MessageStoreDataCidMismatch` - * if the data stream resulted in a data CID that mismatches with `dataCid` in the given message - * @throws {DwnError} with `DwnErrorCode.MessageStoreDataNotFound` - * if `dataCid` in `descriptor` is given, and `dataStream` is not given, and data for the message does not exist already */ - put(messageJson: BaseMessage, indexes: { [key: string]: string }, dataStream?: Readable): Promise; + put(messageJson: BaseMessage, indexes: { [key: string]: string }): Promise; /** - * fetches a single message by `cid` from the underlying store. Returns `undefined` - * if no message was found + * Fetches a single message by `cid` from the underlying store. + * Returns `undefined` no message was found. */ - get(cid: string): Promise; + get(cid: string): Promise; /** * Queries the underlying store for messages that match the query provided. diff --git a/src/store/storage-controller.ts b/src/store/storage-controller.ts new file mode 100644 index 000000000..5181f5294 --- /dev/null +++ b/src/store/storage-controller.ts @@ -0,0 +1,86 @@ +import { BaseMessage } from '../core/types.js'; +import { DataStore } from './data-store.js'; +import { Encoder } from '../index.js'; +import { MessageStore } from './message-store.js'; +import { RangeCriterion } from '../interfaces/records/types.js'; +import { Readable } from 'readable-stream'; + +import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; + +/** + * A class that provides an abstraction for the usage of BlockStore and DataStore. + */ +export class StorageController { + /** + * Puts the given message and data in storage. + * @throws {DwnError} with `DwnErrorCode.MessageStoreDataCidMismatch` + * if the data stream resulted in a data CID that mismatches with `dataCid` in the given message + * @throws {DwnError} with `DwnErrorCode.MessageStoreDataNotFound` + * if `dataCid` in `descriptor` is given, and `dataStream` is not given, and data for the message does not exist already + */ + public static async put( + messageStore: MessageStore, + dataStore: DataStore, + message: BaseMessage, + indexes: { [key: string]: string }, + dataStream?: Readable + ): Promise { + // if `dataCid` is given, it means there is corresponding data associated with this message + // but NOTE: it is possible that a data stream is not given in such case, for instance, + // a subsequent RecordsWrite that changes the `published` property, but the data hasn't changed, + // in this case requiring re-uploading of the data is extremely inefficient so we take care allow omission of data stream + if (message.descriptor.dataCid !== undefined) { + if (dataStream === undefined) { + // the message implies that the data is already in the DB, so we check to make sure the data already exist + // TODO: #218 - Use tenant + record scoped IDs - https://github.com/TBD54566975/dwn-sdk-js/issues/218 + const hasData = await dataStore.has('not used yet', 'not used yet', message.descriptor.dataCid); + + if (!hasData) { + throw new DwnError( + DwnErrorCode.MessageStoreDataNotFound, + `data with dataCid ${message.descriptor.dataCid} not found in store` + ); + } + } else { + const actualDataCid = await dataStore.put('not used yet', 'not used yet', dataStream ); + + // MUST verify that the CID of the actual data matches with the given `dataCid` + // if data CID is wrong, delete the data we just stored + if (message.descriptor.dataCid !== actualDataCid) { + // there is an opportunity to improve here: handle the edge cae of if the delete fails... + await dataStore.delete('not used yet', 'not used yet', actualDataCid); + throw new DwnError( + DwnErrorCode.MessageStoreDataCidMismatch, + `actual data CID ${actualDataCid} does not match dataCid in descriptor: ${message.descriptor.dataCid}` + ); + } + } + } + + await messageStore.put(message, indexes); + } + + public static async query( + messageStore: MessageStore, + dataStore: DataStore, + exactCriteria: { [key: string]: string }, + rangeCriteria?: { [key: string]: RangeCriterion } + ): Promise { + + const messages = await messageStore.query(exactCriteria, rangeCriteria); + + for (const message of messages) { + const dataCid = message.descriptor.dataCid; + if (dataCid !== undefined) { + // TODO: #219 (https://github.com/TBD54566975/dwn-sdk-js/issues/219) + // temporary placeholder for keeping status-quo of returning data in `encodedData` + // once #219 is implemented, `encodedData` may or may not exist directly as part of the returned message here + const dataBytes = await dataStore.get('not used yet', 'not used yet', dataCid); + + message['encodedData'] = Encoder.bytesToBase64Url(dataBytes); + } + } + + return messages; + } +} diff --git a/tests/dwn.spec.ts b/tests/dwn.spec.ts index d6b9eb083..5ab056338 100644 --- a/tests/dwn.spec.ts +++ b/tests/dwn.spec.ts @@ -2,17 +2,19 @@ import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import chai, { expect } from 'chai'; +import { DataStoreLevel } from '../src/store/data-store-level.js'; import { DidKeyResolver } from '../src/did/did-key-resolver.js'; import { Dwn } from '../src/dwn.js'; import { Message } from '../src/core/message.js'; import { MessageStoreLevel } from '../src/store/message-store-level.js'; +import { TenantGate } from '../src/index.js'; import { TestDataGenerator } from './utils/test-data-generator.js'; -import { DwnConfig, TenantGate } from '../src/index.js'; chai.use(chaiAsPromised); describe('DWN', () => { let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; let dwn: Dwn; before(async () => { @@ -23,10 +25,9 @@ describe('DWN', () => { indexLocation : 'TEST-INDEX' }); - await messageStore.open(); + dataStore = new DataStoreLevel('TEST-DATASTORE'); - const dwnConfig: DwnConfig = { messageStore }; - dwn = await Dwn.create(dwnConfig); + dwn = await Dwn.create({ messageStore, dataStore }); }); beforeEach(async () => { @@ -34,8 +35,7 @@ describe('DWN', () => { }); after(async () => { - await messageStore.close(); - dwn.close(); + await dwn.close(); }); describe('create()', () => { @@ -120,7 +120,10 @@ describe('DWN', () => { } }; - const dwnWithConfig = await Dwn.create({ tenantGate: blockAllTenantGate, messageStore }); // block all tenants + const messageStoreStub = sinon.createStubInstance(MessageStoreLevel); + const dataStoreStub = sinon.createStubInstance(DataStoreLevel); + + const dwnWithConfig = await Dwn.create({ tenantGate: blockAllTenantGate, messageStore: messageStoreStub, dataStore: dataStoreStub }); const alice = await DidKeyResolver.generate(); const { requester, message } = await TestDataGenerator.generateRecordsQuery({ requester: alice }); diff --git a/tests/interfaces/protocols/handlers/protocols-configure.spec.ts b/tests/interfaces/protocols/handlers/protocols-configure.spec.ts index 696ee0e72..f597e7b55 100644 --- a/tests/interfaces/protocols/handlers/protocols-configure.spec.ts +++ b/tests/interfaces/protocols/handlers/protocols-configure.spec.ts @@ -2,44 +2,51 @@ import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import chai, { expect } from 'chai'; +import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; import { GeneralJwsSigner } from '../../../../src/jose/jws/general/signer.js'; -import { handleProtocolsConfigure } from '../../../../src/interfaces/protocols/handlers/protocols-configure.js'; -import { handleProtocolsQuery } from '../../../../src/interfaces/protocols/handlers/protocols-query.js'; import { lexicographicalCompare } from '../../../../src/utils/string.js'; import { Message } from '../../../../src/core/message.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; import { TestStubGenerator } from '../../../utils/test-stub-generator.js'; import { GenerateProtocolsConfigureOutput, TestDataGenerator } from '../../../utils/test-data-generator.js'; -import { DidResolver, Encoder, Jws } from '../../../../src/index.js'; +import { DidResolver, Dwn, Encoder, Jws } from '../../../../src/index.js'; chai.use(chaiAsPromised); describe('handleProtocolsQuery()', () => { - describe('functional tests', () => { - let didResolver: DidResolver; - let messageStore: MessageStoreLevel; + let didResolver: DidResolver; + let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; + let dwn: Dwn; + describe('functional tests', () => { before(async () => { didResolver = new DidResolver([new DidKeyResolver()]); - // important to follow this pattern to initialize the message store in tests + // 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-BLOCKSTORE', indexLocation : 'TEST-INDEX' }); - await messageStore.open(); + dataStore = new DataStoreLevel('TEST-DATASTORE'); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore }); }); beforeEach(async () => { - await messageStore.clear(); // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + + // 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(); }); after(async () => { - await messageStore.close(); + await dwn.close(); }); it('should return 400 if more than 1 signature is provided in `authorization`', async () => { @@ -56,9 +63,9 @@ describe('handleProtocolsQuery()', () => { const signer = await GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput1, signatureInput2]); message.authorization = signer.getJws(); - const didResolver = TestStubGenerator.createDidResolverStub(requester); - const messageStore = sinon.createStubInstance(MessageStoreLevel); - const reply = await handleProtocolsConfigure({ tenant, message, messageStore, didResolver }); + TestStubGenerator.stubDidResolver(didResolver, [requester]); + + const reply = await dwn.processMessage(tenant, message); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('expected no more than 1 signature'); @@ -69,7 +76,7 @@ describe('handleProtocolsQuery()', () => { alice.keyId = 'wrongValue'; // to fail authentication const { message } = await TestDataGenerator.generateProtocolsConfigure({ requester: alice }); - const reply = await handleProtocolsConfigure({ tenant: alice.did, message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, message); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('not a valid DID'); }); @@ -97,26 +104,20 @@ describe('handleProtocolsQuery()', () => { = messageDataWithCid.sort((messageDataA, messageDataB) => { return lexicographicalCompare(messageDataA.cid, messageDataB.cid); }); // write the protocol with the middle lexicographic value - let reply = await handleProtocolsConfigure({ - tenant: alice.did, message: middleWrite.message, messageStore, didResolver, dataStream: middleWrite.dataStream - }); + let reply = await dwn.processMessage(alice.did, middleWrite.message, middleWrite.dataStream); expect(reply.status.code).to.equal(202); // test that the protocol with the smallest lexicographic value cannot be written - reply = await handleProtocolsConfigure({ - tenant: alice.did, message: oldestWrite.message, messageStore, didResolver, dataStream: oldestWrite.dataStream - }); + reply = await dwn.processMessage(alice.did, oldestWrite.message, oldestWrite.dataStream); expect(reply.status.code).to.equal(409); // test that the protocol with the largest lexicographic value can be written - reply = await handleProtocolsConfigure({ - tenant: alice.did, message: newestWrite.message, messageStore, didResolver, dataStream: newestWrite.dataStream - }); + reply = await dwn.processMessage(alice.did, newestWrite.message, newestWrite.dataStream); expect(reply.status.code).to.equal(202); // test that old protocol message is removed from DB and only the newer protocol message remains const queryMessageData = await TestDataGenerator.generateProtocolsQuery({ requester: alice, filter: { protocol } }); - reply = await handleProtocolsQuery({ tenant: alice.did, message: queryMessageData.message, messageStore, didResolver }); + reply = await dwn.processMessage(alice.did, queryMessageData.message); expect(reply.status.code).to.equal(200); expect(reply.entries.length).to.equal(1); diff --git a/tests/interfaces/protocols/handlers/protocols-query.spec.ts b/tests/interfaces/protocols/handlers/protocols-query.spec.ts index e76ac3bd3..3ba404d3b 100644 --- a/tests/interfaces/protocols/handlers/protocols-query.spec.ts +++ b/tests/interfaces/protocols/handlers/protocols-query.spec.ts @@ -2,64 +2,65 @@ import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import chai, { expect } from 'chai'; +import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; import { GeneralJwsSigner } from '../../../../src/jose/jws/general/signer.js'; -import { handleProtocolsConfigure } from '../../../../src/interfaces/protocols/handlers/protocols-configure.js'; -import { handleProtocolsQuery } from '../../../../src/interfaces/protocols/handlers/protocols-query.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; import { TestDataGenerator } from '../../../utils/test-data-generator.js'; import { TestStubGenerator } from '../../../utils/test-stub-generator.js'; -import { DidResolver, Encoder, Jws } from '../../../../src/index.js'; +import { DidResolver, Dwn, Encoder, Jws } from '../../../../src/index.js'; chai.use(chaiAsPromised); describe('handleProtocolsQuery()', () => { - describe('functional tests', () => { - let didResolver: DidResolver; - let messageStore: MessageStoreLevel; + let didResolver: DidResolver; + let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; + let dwn: Dwn; + describe('functional tests', () => { before(async () => { didResolver = new DidResolver([new DidKeyResolver()]); - // important to follow this pattern to initialize the message store in tests + // 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-BLOCKSTORE', indexLocation : 'TEST-INDEX' }); - await messageStore.open(); + dataStore = new DataStoreLevel('TEST-DATASTORE'); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore }); }); beforeEach(async () => { - await messageStore.clear(); // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + + // 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(); }); after(async () => { - await messageStore.close(); + await dwn.close(); }); it('should return protocols matching the query', async () => { const alice = await TestDataGenerator.generatePersona(); // setting up a stub method resolver - const didResolver = TestStubGenerator.createDidResolverStub(alice); + TestStubGenerator.stubDidResolver(didResolver, [alice]); // insert three messages into DB, two with matching protocol const protocol1 = await TestDataGenerator.generateProtocolsConfigure({ requester: alice }); const protocol2 = await TestDataGenerator.generateProtocolsConfigure({ requester: alice }); const protocol3 = await TestDataGenerator.generateProtocolsConfigure({ requester: alice }); - await handleProtocolsConfigure({ - tenant: alice.did, message: protocol1.message, messageStore, didResolver, dataStream: protocol1.dataStream - }); - await handleProtocolsConfigure({ - tenant: alice.did, message: protocol2.message, messageStore, didResolver, dataStream: protocol2.dataStream - }); - await handleProtocolsConfigure({ - tenant: alice.did, message: protocol3.message, messageStore, didResolver, dataStream: protocol3.dataStream - }); + await dwn.processMessage(alice.did, protocol1.message, protocol1.dataStream); + await dwn.processMessage(alice.did, protocol2.message, protocol2.dataStream); + await dwn.processMessage(alice.did, protocol3.message, protocol3.dataStream); // testing singular conditional query const queryMessageData = await TestDataGenerator.generateProtocolsQuery({ @@ -67,7 +68,7 @@ describe('handleProtocolsQuery()', () => { filter : { protocol: protocol1.message.descriptor.protocol } }); - const reply = await handleProtocolsQuery({ tenant: alice.did, message: queryMessageData.message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, queryMessageData.message); expect(reply.status.code).to.equal(200); expect(reply.entries?.length).to.equal(1); // only 1 entry should match the query on protocol @@ -77,7 +78,7 @@ describe('handleProtocolsQuery()', () => { requester: alice }); - const reply2 = await handleProtocolsQuery({ tenant: alice.did, message: queryMessageData2.message, messageStore, didResolver }); + const reply2 = await dwn.processMessage(alice.did, queryMessageData2.message); expect(reply2.status.code).to.equal(200); expect(reply2.entries?.length).to.equal(3); // expecting all 3 entries written above match the query @@ -96,9 +97,7 @@ describe('handleProtocolsQuery()', () => { const signer = await GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput]); message.authorization = signer.getJws(); - const didResolver = TestStubGenerator.createDidResolverStub(requester); - const messageStore = sinon.createStubInstance(MessageStoreLevel); - const reply = await handleProtocolsQuery({ tenant, message, messageStore, didResolver }); + const reply = await dwn.processMessage(tenant, message); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain(`${incorrectDescriptorCid} does not match expected CID`); @@ -109,7 +108,7 @@ describe('handleProtocolsQuery()', () => { alice.keyId = 'wrongValue'; // to fail authentication const { message } = await TestDataGenerator.generateProtocolsQuery({ requester: alice }); - const reply = await handleProtocolsQuery({ tenant: alice.did, message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, message); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('not a valid DID'); diff --git a/tests/interfaces/records/handlers/records-delete.spec.ts b/tests/interfaces/records/handlers/records-delete.spec.ts index babde5971..f7a8e68c2 100644 --- a/tests/interfaces/records/handlers/records-delete.spec.ts +++ b/tests/interfaces/records/handlers/records-delete.spec.ts @@ -2,45 +2,50 @@ import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import chai, { expect } from 'chai'; +import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; -import { handleRecordsDelete } from '../../../../src/interfaces/records/handlers/records-delete.js'; -import { handleRecordsQuery } from '../../../../src/interfaces/records/handlers/records-query.js'; -import { handleRecordsWrite } from '../../../../src/interfaces/records/handlers/records-write.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; +import { RecordsDeleteHandler } from '../../../../src/interfaces/records/handlers/records-delete.js'; import { TestDataGenerator } from '../../../utils/test-data-generator.js'; import { TestStubGenerator } from '../../../utils/test-stub-generator.js'; export { RecordsDelete, RecordsDeleteOptions } from '../../../../src/interfaces/records/messages/records-delete.js'; -import { DidResolver, Encoder, Jws, RecordsDelete } from '../../../../src/index.js'; +import { DidResolver, Dwn, Encoder, Jws, RecordsDelete } from '../../../../src/index.js'; chai.use(chaiAsPromised); describe('handleRecordsDelete()', () => { let didResolver: DidResolver; + let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; + let dwn: Dwn; describe('functional tests', () => { - let messageStore: MessageStoreLevel; - before(async () => { - // important to follow this pattern to initialize the message store in tests + 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-BLOCKSTORE', indexLocation : 'TEST-INDEX' }); - await messageStore.open(); + dataStore = new DataStoreLevel('TEST-DATASTORE'); - didResolver = new DidResolver([new DidKeyResolver()]); + dwn = await Dwn.create({ didResolver, messageStore, dataStore }); }); beforeEach(async () => { sinon.restore(); // wipe all previous stubs/spies/mocks/fakes - await messageStore.clear(); // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + + // 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(); }); after(async () => { - await messageStore.close(); + await dwn.close(); }); it('should handle RecordsDelete successfully', async () => { @@ -49,7 +54,7 @@ describe('handleRecordsDelete()', () => { // insert data const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); - const writeReply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver, dataStream }); + const writeReply = await dwn.processMessage(alice.did, message, dataStream); expect(writeReply.status.code).to.equal(202); // ensure data is inserted @@ -58,7 +63,7 @@ describe('handleRecordsDelete()', () => { filter : { recordId: message.recordId } }); - const reply = await handleRecordsQuery({ tenant: alice.did, message: queryData.message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, queryData.message); expect(reply.status.code).to.equal(200); expect(reply.entries?.length).to.equal(1); @@ -68,11 +73,11 @@ describe('handleRecordsDelete()', () => { authorizationSignatureInput : Jws.createSignatureInput(alice) }); - const deleteReply = await handleRecordsDelete({ tenant: alice.did, message: recordsDelete.message, messageStore, didResolver }); + const deleteReply = await dwn.processMessage(alice.did, recordsDelete.message); expect(deleteReply.status.code).to.equal(202); // ensure a query will no longer find the deleted record - const reply2 = await handleRecordsQuery({ tenant: alice.did, message: queryData.message, messageStore, didResolver }); + const reply2 = await dwn.processMessage(alice.did, queryData.message); expect(reply2.status.code).to.equal(200); expect(reply2.entries?.length).to.equal(0); }); @@ -82,9 +87,7 @@ describe('handleRecordsDelete()', () => { // initial write const initialWriteData = await TestDataGenerator.generateRecordsWrite({ requester: alice }); - const initialWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: initialWriteData.message, messageStore, didResolver, dataStream: initialWriteData.dataStream - }); + const initialWriteReply = await dwn.processMessage(alice.did, initialWriteData.message, initialWriteData.dataStream); expect(initialWriteReply.status.code).to.equal(202); // generate subsequent write and delete with the delete having an earlier timestamp @@ -99,13 +102,11 @@ describe('handleRecordsDelete()', () => { }); // subsequent write - const subsequentWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: subsequentWriteData.message, messageStore, didResolver, dataStream: subsequentWriteData.dataStream - }); + const subsequentWriteReply = await dwn.processMessage(alice.did, subsequentWriteData.message, subsequentWriteData.dataStream); expect(subsequentWriteReply.status.code).to.equal(202); // test that a delete with an earlier `dateModified` results in a 409 - const deleteReply = await handleRecordsDelete({ tenant: alice.did, message: recordsDelete.message, messageStore, didResolver }); + const deleteReply = await dwn.processMessage(alice.did, recordsDelete.message); expect(deleteReply.status.code).to.equal(409); // ensure data still exists @@ -114,7 +115,7 @@ describe('handleRecordsDelete()', () => { filter : { recordId: initialWriteData.message.recordId } }); const expectedEncodedData = Encoder.bytesToBase64Url(subsequentWriteData.dataBytes); - const reply = await handleRecordsQuery({ tenant: alice.did, message: queryData.message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, queryData.message); expect(reply.status.code).to.equal(200); expect(reply.entries?.length).to.equal(1); expect((reply.entries[0] as any).encodedData).to.equal(expectedEncodedData); @@ -123,16 +124,17 @@ describe('handleRecordsDelete()', () => { it('should return 401 if signature check fails', async () => { const { requester, message } = await TestDataGenerator.generateRecordsDelete(); + const tenant = requester.did; // setting up a stub did resolver & message store // intentionally not supplying the public key so a different public key is generated to simulate invalid signature const mismatchingPersona = await TestDataGenerator.generatePersona({ did: requester.did, keyId: requester.keyId }); const didResolver = TestStubGenerator.createDidResolverStub(mismatchingPersona); const messageStore = sinon.createStubInstance(MessageStoreLevel); + const dataStore = sinon.createStubInstance(DataStoreLevel); - const tenant = requester.did; - const reply = await handleRecordsDelete({ tenant, message, messageStore, didResolver }); - + const recordsDeleteHandler = new RecordsDeleteHandler(didResolver, messageStore, dataStore); + const reply = await recordsDeleteHandler.handle({ tenant, message }); expect(reply.status.code).to.equal(401); }); @@ -142,10 +144,13 @@ describe('handleRecordsDelete()', () => { // setting up a stub method resolver & message store const messageStore = sinon.createStubInstance(MessageStoreLevel); + const dataStore = sinon.createStubInstance(DataStoreLevel); + + const recordsDeleteHandler = new RecordsDeleteHandler(didResolver, messageStore, dataStore); // stub the `parse()` function to throw an error sinon.stub(RecordsDelete, 'parse').throws('anyError'); - const reply = await handleRecordsDelete({ tenant, message, messageStore, didResolver }); + const reply = await recordsDeleteHandler.handle({ tenant, message }); expect(reply.status.code).to.equal(400); }); diff --git a/tests/interfaces/records/handlers/records-query.spec.ts b/tests/interfaces/records/handlers/records-query.spec.ts index f8ae71cfc..507316bbc 100644 --- a/tests/interfaces/records/handlers/records-query.spec.ts +++ b/tests/interfaces/records/handlers/records-query.spec.ts @@ -2,18 +2,20 @@ import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import chai, { expect } from 'chai'; +import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; import { Encoder } from '../../../../src/utils/encoder.js'; -import { handleRecordsQuery } from '../../../../src/interfaces/records/handlers/records-query.js'; import { Jws } from '../../../../src/utils/jws.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; +import { RecordsQueryHandler } from '../../../../src/interfaces/records/handlers/records-query.js'; +import { StorageController } from '../../../../src/store/storage-controller.js'; import { Temporal } from '@js-temporal/polyfill'; import { TestDataGenerator } from '../../../utils/test-data-generator.js'; import { TestStubGenerator } from '../../../utils/test-stub-generator.js'; -import { constructRecordsWriteIndexes, handleRecordsWrite } from '../../../../src/interfaces/records/handlers/records-write.js'; +import { constructRecordsWriteIndexes } from '../../../../src/interfaces/records/handlers/records-write.js'; import { DateSort, RecordsQuery } from '../../../../src/interfaces/records/messages/records-query.js'; -import { DidResolver, RecordsWriteMessage } from '../../../../src/index.js'; +import { DidResolver, Dwn, RecordsWriteMessage } from '../../../../src/index.js'; chai.use(chaiAsPromised); @@ -21,27 +23,34 @@ describe('handleRecordsQuery()', () => { describe('functional tests', () => { let didResolver: DidResolver; let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; + let dwn: Dwn; before(async () => { - // important to follow this pattern to initialize the message store in tests + 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-BLOCKSTORE', indexLocation : 'TEST-INDEX' }); - await messageStore.open(); + dataStore = new DataStoreLevel('TEST-DATASTORE'); - didResolver = new DidResolver([new DidKeyResolver()]); + dwn = await Dwn.create({ didResolver, messageStore, dataStore }); }); beforeEach(async () => { sinon.restore(); // wipe all previous stubs/spies/mocks/fakes - await messageStore.clear(); // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + + // 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(); }); after(async () => { - await messageStore.close(); + await dwn.close(); }); it('should return records matching the query', async () => { @@ -52,19 +61,14 @@ describe('handleRecordsQuery()', () => { const write2 = await TestDataGenerator.generateRecordsWrite({ requester: alice, dataFormat, schema: 'schema1' }); const write3 = await TestDataGenerator.generateRecordsWrite({ requester: alice, dataFormat, schema: 'schema2' }); - // setting up a stub method resolver - const didResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub resolver + const mockResolution = TestDataGenerator.createDidResolutionResult(alice);; + sinon.stub(didResolver, 'resolve').resolves(mockResolution); // insert data - const writeReply1 = await handleRecordsWrite({ - tenant: alice.did, message: write1.message, messageStore, didResolver, dataStream: write1.dataStream - }); - const writeReply2 = await handleRecordsWrite({ - tenant: alice.did, message: write2.message, messageStore, didResolver, dataStream: write2.dataStream - }); - const writeReply3 = await handleRecordsWrite({ - tenant: alice.did, message: write3.message, messageStore, didResolver, dataStream: write3.dataStream - }); + const writeReply1 = await dwn.processMessage(alice.did, write1.message, write1.dataStream); + const writeReply2 = await dwn.processMessage(alice.did, write2.message, write2.dataStream); + const writeReply3 = await dwn.processMessage(alice.did, write3.message, write3.dataStream); expect(writeReply1.status.code).to.equal(202); expect(writeReply2.status.code).to.equal(202); expect(writeReply3.status.code).to.equal(202); @@ -72,7 +76,7 @@ describe('handleRecordsQuery()', () => { // testing singular conditional query const messageData = await TestDataGenerator.generateRecordsQuery({ requester: alice, filter: { dataFormat } }); - const reply = await handleRecordsQuery({ tenant: alice.did, message: messageData.message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, messageData.message); expect(reply.status.code).to.equal(200); expect(reply.entries?.length).to.equal(2); // only 2 entries should match the query on protocol @@ -86,7 +90,7 @@ describe('handleRecordsQuery()', () => { } }); - const reply2 = await handleRecordsQuery({ tenant: alice.did, message: messageData2.message, messageStore, didResolver }); + const reply2 = await dwn.processMessage(alice.did, messageData2.message); expect(reply2.status.code).to.equal(200); expect(reply2.entries?.length).to.equal(1); // only 1 entry should match the query @@ -100,18 +104,14 @@ describe('handleRecordsQuery()', () => { const recordsWrite2 = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [bob] }); // insert data - const writeReply1 = await handleRecordsWrite({ - tenant: alice.did, message: recordsWrite1.message, messageStore, didResolver, dataStream: recordsWrite1.dataStream - }); - const writeReply2 = await handleRecordsWrite({ - tenant: alice.did, message: recordsWrite2.message, messageStore, didResolver, dataStream: recordsWrite2.dataStream - }); + const writeReply1 = await dwn.processMessage(alice.did, recordsWrite1.message, recordsWrite1.dataStream); + const writeReply2 = await dwn.processMessage(alice.did, recordsWrite2.message, recordsWrite2.dataStream); expect(writeReply1.status.code).to.equal(202); expect(writeReply2.status.code).to.equal(202); // testing attester filter const recordsQuery1 = await TestDataGenerator.generateRecordsQuery({ requester: alice, filter: { attester: alice.did } }); - const reply1 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery1.message, messageStore, didResolver }); + const reply1 = await dwn.processMessage(alice.did, recordsQuery1.message); expect(reply1.entries?.length).to.equal(1); const reply1Attester = Jws.getSignerDid((reply1.entries[0] as RecordsWriteMessage).attestation.signatures[0]); expect(reply1Attester).to.equal(alice.did); @@ -121,7 +121,7 @@ describe('handleRecordsQuery()', () => { requester : alice, filter : { attester: bob.did, schema: recordsWrite2.message.descriptor.schema } }); - const reply2 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery2.message, messageStore, didResolver }); + const reply2 = await dwn.processMessage(alice.did, recordsQuery2.message); expect(reply2.entries?.length).to.equal(1); const reply2Attester = Jws.getSignerDid((reply2.entries[0] as RecordsWriteMessage).attestation.signatures[0]); expect(reply2Attester).to.equal(bob.did); @@ -129,7 +129,7 @@ describe('handleRecordsQuery()', () => { // testing attester filter that yields no results const carol = await DidKeyResolver.generate(); const recordsQuery3 = await TestDataGenerator.generateRecordsQuery({ requester: alice, filter: { attester: carol.did } }); - const reply3 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery3.message, messageStore, didResolver }); + const reply3 = await dwn.processMessage(alice.did, recordsQuery3.message); expect(reply3.entries?.length).to.equal(0); }); @@ -144,15 +144,9 @@ describe('handleRecordsQuery()', () => { const write3 = await TestDataGenerator.generateRecordsWrite({ requester: alice, dateCreated: firstDayOf2023, dateModified: firstDayOf2023 }); // insert data - const writeReply1 = await handleRecordsWrite({ - tenant: alice.did, message: write1.message, messageStore, didResolver, dataStream: write1.dataStream - }); - const writeReply2 = await handleRecordsWrite({ - tenant: alice.did, message: write2.message, messageStore, didResolver, dataStream: write2.dataStream - }); - const writeReply3 = await handleRecordsWrite({ - tenant: alice.did, message: write3.message, messageStore, didResolver, dataStream: write3.dataStream - }); + const writeReply1 = await dwn.processMessage(alice.did, write1.message, write1.dataStream); + const writeReply2 = await dwn.processMessage(alice.did, write2.message, write2.dataStream); + const writeReply3 = await dwn.processMessage(alice.did, write3.message, write3.dataStream); expect(writeReply1.status.code).to.equal(202); expect(writeReply2.status.code).to.equal(202); expect(writeReply3.status.code).to.equal(202); @@ -164,7 +158,7 @@ describe('handleRecordsQuery()', () => { filter : { dateCreated: { from: lastDayOf2021 } }, dateSort : DateSort.CreatedAscending }); - const reply1 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery1.message, messageStore, didResolver }); + const reply1 = await dwn.processMessage(alice.did, recordsQuery1.message); expect(reply1.entries?.length).to.equal(2); expect((reply1.entries[0] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write2.dataBytes)); expect((reply1.entries[1] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write3.dataBytes)); @@ -176,7 +170,7 @@ describe('handleRecordsQuery()', () => { filter : { dateCreated: { to: lastDayOf2022 } }, dateSort : DateSort.CreatedAscending }); - const reply2 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery2.message, messageStore, didResolver }); + const reply2 = await dwn.processMessage(alice.did, recordsQuery2.message); expect(reply2.entries?.length).to.equal(2); expect((reply2.entries[0] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write1.dataBytes)); expect((reply2.entries[1] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write2.dataBytes)); @@ -188,7 +182,7 @@ describe('handleRecordsQuery()', () => { filter : { dateCreated: { from: lastDayOf2022, to: lastDayOf2023 } }, dateSort : DateSort.CreatedAscending }); - const reply3 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery3.message, messageStore, didResolver }); + const reply3 = await dwn.processMessage(alice.did, recordsQuery3.message); expect(reply3.entries?.length).to.equal(1); expect((reply3.entries[0] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write3.dataBytes)); @@ -198,7 +192,7 @@ describe('handleRecordsQuery()', () => { filter : { dateCreated: { from: firstDayOf2022, to: firstDayOf2023 } }, dateSort : DateSort.CreatedAscending }); - const reply4 = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery4.message, messageStore, didResolver }); + const reply4 = await dwn.processMessage(alice.did, recordsQuery4.message); expect(reply4.entries?.length).to.equal(2); expect((reply4.entries[0] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write2.dataBytes)); expect((reply4.entries[1] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write3.dataBytes)); @@ -222,15 +216,9 @@ describe('handleRecordsQuery()', () => { }); // insert data - const writeReply1 = await handleRecordsWrite({ - tenant: alice.did, message: write1.message, messageStore, didResolver, dataStream: write1.dataStream - }); - const writeReply2 = await handleRecordsWrite({ - tenant: alice.did, message: write2.message, messageStore, didResolver, dataStream: write2.dataStream - }); - const writeReply3 = await handleRecordsWrite({ - tenant: alice.did, message: write3.message, messageStore, didResolver, dataStream: write3.dataStream - }); + const writeReply1 = await dwn.processMessage(alice.did, write1.message, write1.dataStream); + const writeReply2 = await dwn.processMessage(alice.did, write2.message, write2.dataStream); + const writeReply3 = await dwn.processMessage(alice.did, write3.message, write3.dataStream); expect(writeReply1.status.code).to.equal(202); expect(writeReply2.status.code).to.equal(202); expect(writeReply3.status.code).to.equal(202); @@ -246,7 +234,7 @@ describe('handleRecordsQuery()', () => { }, dateSort: DateSort.CreatedAscending }); - const reply = await handleRecordsQuery({ tenant: alice.did, message: recordsQuery5.message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, recordsQuery5.message); expect(reply.entries?.length).to.equal(1); expect((reply.entries[0] as any).encodedData).to.equal(Encoder.bytesToBase64Url(write2.dataBytes)); }); @@ -256,9 +244,10 @@ describe('handleRecordsQuery()', () => { const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); // setting up a stub method resolver - const didResolver = TestStubGenerator.createDidResolverStub(alice); + const mockResolution = TestDataGenerator.createDidResolutionResult(alice);; + sinon.stub(didResolver, 'resolve').resolves(mockResolution); - const writeReply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver, dataStream }); + const writeReply = await dwn.processMessage(alice.did, message, dataStream); expect(writeReply.status.code).to.equal(202); const queryData = await TestDataGenerator.generateRecordsQuery({ @@ -266,7 +255,7 @@ describe('handleRecordsQuery()', () => { filter : { schema: message.descriptor.schema } }); - const queryReply = await handleRecordsQuery({ tenant: alice.did, message: queryData.message, messageStore, didResolver }); + const queryReply = await dwn.processMessage(alice.did, queryData.message); expect(queryReply.status.code).to.equal(200); expect(queryReply.entries?.length).to.equal(1); expect(queryReply.entries[0]['authorization']).to.equal(undefined); @@ -278,7 +267,7 @@ describe('handleRecordsQuery()', () => { const alice = await DidKeyResolver.generate(); const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [alice] }); - const writeReply = await handleRecordsWrite({ tenant: alice.did, message: message, messageStore, didResolver, dataStream }); + const writeReply = await dwn.processMessage(alice.did, message, dataStream); expect(writeReply.status.code).to.equal(202); const queryData = await TestDataGenerator.generateRecordsQuery({ @@ -286,7 +275,7 @@ describe('handleRecordsQuery()', () => { filter : { schema: message.descriptor.schema } }); - const queryReply = await handleRecordsQuery({ tenant: alice.did, message: queryData.message, messageStore, didResolver }); + const queryReply = await dwn.processMessage(alice.did, queryData.message); expect(queryReply.status.code).to.equal(200); expect(queryReply.entries?.length).to.equal(1); @@ -306,15 +295,12 @@ describe('handleRecordsQuery()', () => { }); // setting up a stub method resolver - const didResolver = TestStubGenerator.createDidResolverStub(alice); + const mockResolution = TestDataGenerator.createDidResolutionResult(alice);; + sinon.stub(didResolver, 'resolve').resolves(mockResolution); // insert data - const publishedWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: publishedWriteData.message, messageStore, didResolver, dataStream: publishedWriteData.dataStream - }); - const unpublishedWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: unpublishedWriteData.message, messageStore, didResolver, dataStream: unpublishedWriteData.dataStream - }); + const publishedWriteReply = await dwn.processMessage(alice.did, publishedWriteData.message, publishedWriteData.dataStream); + const unpublishedWriteReply = await dwn.processMessage(alice.did, unpublishedWriteData.message, unpublishedWriteData.dataStream); expect(publishedWriteReply.status.code).to.equal(202); expect(unpublishedWriteReply.status.code).to.equal(202); @@ -324,9 +310,7 @@ describe('handleRecordsQuery()', () => { dateSort : DateSort.PublishedAscending, filter : { schema } }); - const publishedAscendingQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: publishedAscendingQueryData.message, messageStore, didResolver - }); + const publishedAscendingQueryReply = await dwn.processMessage(alice.did, publishedAscendingQueryData.message); expect(publishedAscendingQueryReply.entries?.length).to.equal(1); expect(publishedAscendingQueryReply.entries[0].descriptor['datePublished']).to.equal(publishedWriteData.message.descriptor.datePublished); @@ -337,9 +321,7 @@ describe('handleRecordsQuery()', () => { dateSort : DateSort.PublishedDescending, filter : { schema } }); - const publishedDescendingQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: publishedDescendingQueryData.message, messageStore, didResolver - }); + const publishedDescendingQueryReply = await dwn.processMessage(alice.did, publishedDescendingQueryData.message); expect(publishedDescendingQueryReply.entries?.length).to.equal(1); expect(publishedDescendingQueryReply.entries[0].descriptor['datePublished']).to.equal(publishedWriteData.message.descriptor.datePublished); @@ -355,18 +337,13 @@ describe('handleRecordsQuery()', () => { const write3Data = await TestDataGenerator.generateRecordsWrite({ requester: alice, schema, published }); // setting up a stub method resolver - const didResolver = TestStubGenerator.createDidResolverStub(alice); + const mockResolution = TestDataGenerator.createDidResolutionResult(alice);; + sinon.stub(didResolver, 'resolve').resolves(mockResolution); // insert data, intentionally out of order - const writeReply2 = await handleRecordsWrite({ - tenant: alice.did, message: write2Data.message, messageStore, didResolver, dataStream: write2Data.dataStream - }); - const writeReply1 = await handleRecordsWrite({ - tenant: alice.did, message: write1Data.message, messageStore, didResolver, dataStream: write1Data.dataStream - }); - const writeReply3 = await handleRecordsWrite({ - tenant: alice.did, message: write3Data.message, messageStore, didResolver, dataStream: write3Data.dataStream - }); + const writeReply2 = await dwn.processMessage(alice.did, write2Data.message, write2Data.dataStream); + const writeReply1 = await dwn.processMessage(alice.did, write1Data.message, write1Data.dataStream); + const writeReply3 = await dwn.processMessage(alice.did, write3Data.message, write3Data.dataStream); expect(writeReply1.status.code).to.equal(202); expect(writeReply2.status.code).to.equal(202); expect(writeReply3.status.code).to.equal(202); @@ -377,9 +354,7 @@ describe('handleRecordsQuery()', () => { dateSort : DateSort.CreatedAscending, filter : { schema } }); - const createdAscendingQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: createdAscendingQueryData.message, messageStore, didResolver - }); + const createdAscendingQueryReply = await dwn.processMessage(alice.did, createdAscendingQueryData.message); expect(createdAscendingQueryReply.entries[0].descriptor['dateCreated']).to.equal(write1Data.message.descriptor.dateCreated); expect(createdAscendingQueryReply.entries[1].descriptor['dateCreated']).to.equal(write2Data.message.descriptor.dateCreated); @@ -391,9 +366,7 @@ describe('handleRecordsQuery()', () => { dateSort : DateSort.CreatedDescending, filter : { schema } }); - const createdDescendingQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: createdDescendingQueryData.message, messageStore, didResolver - }); + const createdDescendingQueryReply = await dwn.processMessage(alice.did, createdDescendingQueryData.message); expect(createdDescendingQueryReply.entries[0].descriptor['dateCreated']).to.equal(write3Data.message.descriptor.dateCreated); expect(createdDescendingQueryReply.entries[1].descriptor['dateCreated']).to.equal(write2Data.message.descriptor.dateCreated); @@ -405,9 +378,7 @@ describe('handleRecordsQuery()', () => { dateSort : DateSort.PublishedAscending, filter : { schema } }); - const publishedAscendingQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: publishedAscendingQueryData.message, messageStore, didResolver - }); + const publishedAscendingQueryReply = await dwn.processMessage(alice.did, publishedAscendingQueryData.message); expect(publishedAscendingQueryReply.entries[0].descriptor['datePublished']).to.equal(write1Data.message.descriptor.datePublished); expect(publishedAscendingQueryReply.entries[1].descriptor['datePublished']).to.equal(write2Data.message.descriptor.datePublished); @@ -419,9 +390,7 @@ describe('handleRecordsQuery()', () => { dateSort : DateSort.PublishedDescending, filter : { schema } }); - const publishedDescendingQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: publishedDescendingQueryData.message, messageStore, didResolver - }); + const publishedDescendingQueryReply = await dwn.processMessage(alice.did, publishedDescendingQueryData.message); expect(publishedDescendingQueryReply.entries[0].descriptor['datePublished']).to.equal(write3Data.message.descriptor.datePublished); expect(publishedDescendingQueryReply.entries[1].descriptor['datePublished']).to.equal(write2Data.message.descriptor.datePublished); @@ -455,10 +424,10 @@ describe('handleRecordsQuery()', () => { const additionalIndexes2 = await constructRecordsWriteIndexes(alice.did, record2Data.recordsWrite, true); const additionalIndexes3 = await constructRecordsWriteIndexes(alice.did, record3Data.recordsWrite, true); const additionalIndexes4 = await constructRecordsWriteIndexes(alice.did, record4Data.recordsWrite, true); - await messageStore.put(record1Data.message, additionalIndexes1, record1Data.dataStream); - await messageStore.put(record2Data.message, additionalIndexes2, record2Data.dataStream); - await messageStore.put(record3Data.message, additionalIndexes3, record3Data.dataStream); - await messageStore.put(record4Data.message, additionalIndexes4, record4Data.dataStream); + await StorageController.put(messageStore, dataStore, record1Data.message, additionalIndexes1, record1Data.dataStream); + await StorageController.put(messageStore, dataStore, record2Data.message, additionalIndexes2, record2Data.dataStream); + await StorageController.put(messageStore, dataStore, record3Data.message, additionalIndexes3, record3Data.dataStream); + await StorageController.put(messageStore, dataStore, record4Data.message, additionalIndexes4, record4Data.dataStream); // test correctness for Bob's query const bobQueryMessageData = await TestDataGenerator.generateRecordsQuery({ @@ -466,7 +435,7 @@ describe('handleRecordsQuery()', () => { filter : { schema } }); - const replyToBob = await handleRecordsQuery({ tenant: alice.did, message: bobQueryMessageData.message, messageStore, didResolver }); + const replyToBob = await dwn.processMessage(alice.did, bobQueryMessageData.message); expect(replyToBob.status.code).to.equal(200); expect(replyToBob.entries?.length).to.equal(3); // expect 3 records @@ -484,7 +453,7 @@ describe('handleRecordsQuery()', () => { filter : { schema } }); - const replyToAliceQuery = await handleRecordsQuery({ tenant: alice.did, message: aliceQueryMessageData.message, messageStore, didResolver }); + const replyToAliceQuery = await dwn.processMessage(alice.did, aliceQueryMessageData.message); expect(replyToAliceQuery.status.code).to.equal(200); expect(replyToAliceQuery.entries?.length).to.equal(4); // expect all 4 records @@ -499,9 +468,7 @@ describe('handleRecordsQuery()', () => { { requester: alice, schema, data: Encoder.stringToBytes('1'), published: false } // explicitly setting `published` to `false` ); - const result1 = await handleRecordsWrite({ - tenant: alice.did, message: unpublishedRecordsWrite.message, messageStore, didResolver, dataStream: unpublishedRecordsWrite.dataStream - }); + const result1 = await dwn.processMessage(alice.did, unpublishedRecordsWrite.message, unpublishedRecordsWrite.dataStream); expect(result1.status.code).to.equal(202); // alice should be able to see the unpublished record @@ -509,7 +476,7 @@ describe('handleRecordsQuery()', () => { requester : alice, filter : { schema } }); - const replyToAliceQuery = await handleRecordsQuery({ tenant: alice.did, message: queryByAlice.message, messageStore, didResolver }); + const replyToAliceQuery = await dwn.processMessage(alice.did, queryByAlice.message); expect(replyToAliceQuery.status.code).to.equal(200); expect(replyToAliceQuery.entries?.length).to.equal(1); @@ -518,7 +485,7 @@ describe('handleRecordsQuery()', () => { requester : bob, filter : { schema } }); - const replyToBobQuery = await handleRecordsQuery({ tenant: alice.did, message: queryByBob.message, messageStore, didResolver }); + const replyToBobQuery = await dwn.processMessage(alice.did, queryByBob.message); expect(replyToBobQuery.status.code).to.equal(200); expect(replyToBobQuery.entries?.length).to.equal(0); }); @@ -533,7 +500,7 @@ describe('handleRecordsQuery()', () => { filter : { recipient: carol.did } // bob querying carol's records }); - const replyToBobQuery = await handleRecordsQuery({ tenant: alice.did, message: bobQueryMessageData.message, messageStore, didResolver }); + const replyToBobQuery = await dwn.processMessage(alice.did, bobQueryMessageData.message); expect(replyToBobQuery.status.code).to.equal(401); expect(replyToBobQuery.status.detail).to.contain('not allowed to query records'); @@ -548,7 +515,7 @@ describe('handleRecordsQuery()', () => { filter : { recipient: bob.did } // alice as the DWN owner querying bob's records }); - const replyToBobQuery = await handleRecordsQuery({ tenant: alice.did, message: bobQueryMessageData.message, messageStore, didResolver }); + const replyToBobQuery = await dwn.processMessage(alice.did, bobQueryMessageData.message); expect(replyToBobQuery.status.code).to.equal(200); }); @@ -567,15 +534,10 @@ describe('handleRecordsQuery()', () => { }); // insert data into 2 different tenants - const didResolver = new DidResolver([new DidKeyResolver()]); - await handleRecordsWrite({ - tenant: alice.did, message: recordsWriteMessage1Data.message, messageStore, didResolver, dataStream: recordsWriteMessage1Data.dataStream - }); - await handleRecordsWrite({ - tenant: bob.did, message: recordsWriteMessage2Data.message, messageStore, didResolver, dataStream: recordsWriteMessage2Data.dataStream - }); + await dwn.processMessage(alice.did, recordsWriteMessage1Data.message, recordsWriteMessage1Data.dataStream); + await dwn.processMessage(bob.did, recordsWriteMessage2Data.message, recordsWriteMessage2Data.dataStream); - const reply = await handleRecordsQuery({ tenant: alice.did, message: aliceQueryMessageData.message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, aliceQueryMessageData.message); expect(reply.status.code).to.equal(200); expect(reply.entries?.length).to.equal(1); @@ -584,15 +546,17 @@ describe('handleRecordsQuery()', () => { it('should return 401 if signature check fails', async () => { const { requester, message } = await TestDataGenerator.generateRecordsQuery(); + const tenant = requester.did; // setting up a stub did resolver & message store // intentionally not supplying the public key so a different public key is generated to simulate invalid signature const mismatchingPersona = await TestDataGenerator.generatePersona({ did: requester.did, keyId: requester.keyId }); const didResolver = TestStubGenerator.createDidResolverStub(mismatchingPersona); const messageStore = sinon.createStubInstance(MessageStoreLevel); + const dataStore = sinon.createStubInstance(DataStoreLevel); - const tenant = requester.did; - const reply = await handleRecordsQuery({ tenant, message, messageStore, didResolver }); + const recordsQueryHandler = new RecordsQueryHandler(didResolver, messageStore, dataStore); + const reply = await recordsQueryHandler.handle({ tenant, message }); expect(reply.status.code).to.equal(401); }); @@ -604,10 +568,12 @@ describe('handleRecordsQuery()', () => { // setting up a stub method resolver & message store const didResolver = TestStubGenerator.createDidResolverStub(requester); const messageStore = sinon.createStubInstance(MessageStoreLevel); + const dataStore = sinon.createStubInstance(DataStoreLevel); + const recordsQueryHandler = new RecordsQueryHandler(didResolver, messageStore, dataStore); // stub the `parse()` function to throw an error sinon.stub(RecordsQuery, 'parse').throws('anyError'); - const reply = await handleRecordsQuery({ tenant, message, messageStore, didResolver }); + const reply = await recordsQueryHandler.handle({ tenant, message }); expect(reply.status.code).to.equal(400); }); diff --git a/tests/interfaces/records/handlers/records-write.spec.ts b/tests/interfaces/records/handlers/records-write.spec.ts index 5e06cfd83..8493a2ba8 100644 --- a/tests/interfaces/records/handlers/records-write.spec.ts +++ b/tests/interfaces/records/handlers/records-write.spec.ts @@ -6,6 +6,7 @@ import chai, { expect } from 'chai'; import { base64url } from 'multiformats/bases/base64'; import { computeCid } from '../../../../src/utils/cid.js'; +import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DataStream } from '../../../../src/utils/data-stream.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; import { DidResolver } from '../../../../src/did/did-resolver.js'; @@ -13,43 +14,50 @@ import { DwnErrorCode } from '../../../../src/core/dwn-error.js'; import { Encoder } from '../../../../src/utils/encoder.js'; import { GeneralJwsSigner } from '../../../../src/jose/jws/general/signer.js'; import { getCurrentTimeInHighPrecision } from '../../../../src/utils/time.js'; -import { handleProtocolsConfigure } from '../../../../src/interfaces/protocols/handlers/protocols-configure.js'; -import { handleRecordsQuery } from '../../../../src/interfaces/records/handlers/records-query.js'; -import { handleRecordsWrite } from '../../../../src/interfaces/records/handlers/records-write.js'; import { Message } from '../../../../src/core/message.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; +import { RecordsWriteHandler } from '../../../../src/interfaces/records/handlers/records-write.js'; import { RecordsWriteMessage } from '../../../../src/interfaces/records/types.js'; +import { StorageController } from '../../../../src/store/storage-controller.js'; import { TestStubGenerator } from '../../../utils/test-stub-generator.js'; +import { Dwn, Jws, ProtocolDefinition, RecordsWrite } from '../../../../src/index.js'; import { GenerateFromRecordsWriteOut, TestDataGenerator } from '../../../utils/test-data-generator.js'; -import { Jws, ProtocolDefinition, RecordsWrite } from '../../../../src/index.js'; chai.use(chaiAsPromised); describe('handleRecordsWrite()', () => { let didResolver: DidResolver; let messageStore: MessageStoreLevel; + let dataStore: DataStoreLevel; + let dwn: Dwn; describe('functional tests', () => { before(async () => { didResolver = new DidResolver([new DidKeyResolver()]); - // important to follow this pattern to initialize the message store in tests + // 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-BLOCKSTORE', indexLocation : 'TEST-INDEX' }); - await messageStore.open(); + dataStore = new DataStoreLevel('TEST-DATASTORE'); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore }); }); beforeEach(async () => { - await messageStore.clear(); // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + + // 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(); }); after(async () => { - await messageStore.close(); + await dwn.close(); }); it('should only be able to overwrite existing record if new record has a later `dateModified` value', async () => { @@ -58,12 +66,8 @@ describe('handleRecordsWrite()', () => { const data1 = new TextEncoder().encode('data1'); const recordsWriteMessageData = await TestDataGenerator.generateRecordsWrite({ requester, data: data1 }); - const didResolver = new DidResolver(); - const tenant = requester.did; - const recordsWriteReply = await handleRecordsWrite({ - tenant, message: recordsWriteMessageData.message, messageStore, didResolver, dataStream: recordsWriteMessageData.dataStream - }); + const recordsWriteReply = await dwn.processMessage(tenant, recordsWriteMessageData.message, recordsWriteMessageData.dataStream); expect(recordsWriteReply.status.code).to.equal(202); const recordId = recordsWriteMessageData.message.recordId; @@ -73,7 +77,7 @@ describe('handleRecordsWrite()', () => { }); // verify the message written can be queried - const recordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const recordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(recordsQueryReply.status.code).to.equal(200); expect(recordsQueryReply.entries?.length).to.equal(1); expect((recordsQueryReply.entries![0] as any).encodedData).to.equal(base64url.baseEncode(data1)); @@ -91,26 +95,22 @@ describe('handleRecordsWrite()', () => { // sanity check that old data and new data are different expect(newDataEncoded).to.not.equal(Encoder.bytesToBase64Url(recordsWriteMessageData.dataBytes)); - const newRecordsWriteReply = await handleRecordsWrite({ - tenant, message: newRecordsWrite.message, messageStore, didResolver, dataStream: newRecordsWrite.dataStream - }); + const newRecordsWriteReply = await dwn.processMessage(tenant, newRecordsWrite.message, newRecordsWrite.dataStream); expect(newRecordsWriteReply.status.code).to.equal(202); // verify new record has overwritten the existing record - const newRecordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const newRecordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(newRecordsQueryReply.status.code).to.equal(200); expect(newRecordsQueryReply.entries?.length).to.equal(1); expect((newRecordsQueryReply.entries![0] as any).encodedData).to.equal(newDataEncoded); // try to write the older message to store again and verify that it is not accepted - const thirdRecordsWriteReply = await handleRecordsWrite({ - tenant, message: recordsWriteMessageData.message, messageStore, didResolver, dataStream: recordsWriteMessageData.dataStream - }); + const thirdRecordsWriteReply = await dwn.processMessage(tenant, recordsWriteMessageData.message, recordsWriteMessageData.dataStream); expect(thirdRecordsWriteReply.status.code).to.equal(409); // expecting to fail // expecting unchanged - const thirdRecordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const thirdRecordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(thirdRecordsQueryReply.status.code).to.equal(200); expect(thirdRecordsQueryReply.entries?.length).to.equal(1); expect((thirdRecordsQueryReply.entries![0] as any).encodedData).to.equal(newDataEncoded); @@ -125,12 +125,10 @@ describe('handleRecordsWrite()', () => { data: Encoder.stringToBytes('unused') }); - // setting up a stub did resolver - const didResolver = TestStubGenerator.createDidResolverStub(requester); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [requester]); - const originatingMessageWriteReply = await handleRecordsWrite({ - tenant, message: originatingMessageData.message, messageStore, didResolver, dataStream: originatingMessageData.dataStream - }); + const originatingMessageWriteReply = await dwn.processMessage(tenant, originatingMessageData.message, originatingMessageData.dataStream); expect(originatingMessageWriteReply.status.code).to.equal(202); // generate two new RecordsWrite messages with the same `dateModified` value @@ -160,9 +158,7 @@ describe('handleRecordsWrite()', () => { } // write the message with the smaller lexicographical message CID first - const recordsWriteReply = await handleRecordsWrite({ - tenant, message: olderWrite.message, messageStore, didResolver, dataStream: olderWrite.dataStream - }); + const recordsWriteReply = await dwn.processMessage(tenant, olderWrite.message, olderWrite.dataStream); expect(recordsWriteReply.status.code).to.equal(202); // query to fetch the record @@ -172,37 +168,33 @@ describe('handleRecordsWrite()', () => { }); // verify the data is written - const recordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const recordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(recordsQueryReply.status.code).to.equal(200); expect(recordsQueryReply.entries?.length).to.equal(1); expect((recordsQueryReply.entries![0] as RecordsWriteMessage).descriptor.dataCid) .to.equal(olderWrite.message.descriptor.dataCid); // attempt to write the message with larger lexicographical message CID - const newRecordsWriteReply = await handleRecordsWrite({ - tenant, message: newerWrite.message, messageStore, didResolver, dataStream: newerWrite.dataStream - }); + const newRecordsWriteReply = await dwn.processMessage(tenant, newerWrite.message, newerWrite.dataStream); expect(newRecordsWriteReply.status.code).to.equal(202); // verify new record has overwritten the existing record - const newRecordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const newRecordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(newRecordsQueryReply.status.code).to.equal(200); expect(newRecordsQueryReply.entries?.length).to.equal(1); expect((newRecordsQueryReply.entries![0] as RecordsWriteMessage).descriptor.dataCid) .to.equal(newerWrite.message.descriptor.dataCid); // try to write the message with smaller lexicographical message CID again - const thirdRecordsWriteReply = await handleRecordsWrite({ + const thirdRecordsWriteReply = await dwn.processMessage( tenant, - message : olderWrite.message, - messageStore, - didResolver, - dataStream : DataStream.fromBytes(olderWrite.dataBytes) // need to create data stream again since it's already used above - }); + olderWrite.message, + DataStream.fromBytes(olderWrite.dataBytes) // need to create data stream again since it's already used above + ); expect(thirdRecordsWriteReply.status.code).to.equal(409); // expecting to fail // verify the message in store is still the one with larger lexicographical message CID - const thirdRecordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const thirdRecordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(thirdRecordsQueryReply.status.code).to.equal(200); expect(thirdRecordsQueryReply.entries?.length).to.equal(1); expect((thirdRecordsQueryReply.entries![0] as RecordsWriteMessage).descriptor.dataCid) @@ -212,10 +204,10 @@ describe('handleRecordsWrite()', () => { it('should not allow changes to immutable properties', async () => { const initialWriteData = await TestDataGenerator.generateRecordsWrite(); const tenant = initialWriteData.requester.did; - const didResolver = TestStubGenerator.createDidResolverStub(initialWriteData.requester); - const initialWriteReply = await handleRecordsWrite({ - tenant, message: initialWriteData.message, messageStore, didResolver, dataStream: initialWriteData.dataStream - }); + + TestStubGenerator.stubDidResolver(didResolver, [initialWriteData.requester]); + + const initialWriteReply = await dwn.processMessage(tenant, initialWriteData.message, initialWriteData.dataStream); expect(initialWriteReply.status.code).to.equal(202); const recordId = initialWriteData.message.recordId; @@ -231,9 +223,7 @@ describe('handleRecordsWrite()', () => { dataFormat : initialWriteData.message.descriptor.dataFormat }); - let reply = await handleRecordsWrite({ - tenant, message: childMessageData.message, messageStore, didResolver, dataStream: childMessageData.dataStream - }); + let reply = await dwn.processMessage(tenant, childMessageData.message, childMessageData.dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('dateCreated is an immutable property'); @@ -247,9 +237,7 @@ describe('handleRecordsWrite()', () => { dataFormat : initialWriteData.message.descriptor.dataFormat }); - reply = await handleRecordsWrite({ - tenant, message: childMessageData.message, messageStore, didResolver, dataStream: childMessageData.dataStream - }); + reply = await dwn.processMessage(tenant, childMessageData.message, childMessageData.dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('schema is an immutable property'); @@ -263,9 +251,7 @@ describe('handleRecordsWrite()', () => { dataFormat : 'should-not-be-allowed-to-change' }); - reply = await handleRecordsWrite({ - tenant, message: childMessageData.message, messageStore, didResolver, dataStream: childMessageData.dataStream - }); + reply = await dwn.processMessage(tenant, childMessageData.message, childMessageData.dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('dataFormat is an immutable property'); @@ -276,7 +262,7 @@ describe('handleRecordsWrite()', () => { const { message } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); const dataStream = DataStream.fromBytes(Encoder.stringToBytes('mismatching data stream')); // mismatch data stream - const reply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver, dataStream }); + const reply = await dwn.processMessage(alice.did, message, dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('does not match dataCid in descriptor'); }); @@ -288,7 +274,7 @@ describe('handleRecordsWrite()', () => { requester: alice, }); - const reply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver }); + const reply = await dwn.processMessage(alice.did, message); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain(DwnErrorCode.MessageStoreDataNotFound); @@ -303,8 +289,9 @@ describe('handleRecordsWrite()', () => { const tenant = requester.did; // setting up a stub DID resolver - const didResolver = TestStubGenerator.createDidResolverStub(requester); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + TestStubGenerator.stubDidResolver(didResolver, [requester]); + + const reply = await dwn.processMessage(tenant, message, dataStream); expect(reply.status.code).to.equal(202); @@ -314,7 +301,7 @@ describe('handleRecordsWrite()', () => { authorizationSignatureInput : Jws.createSignatureInput(requester) }); - const newWriteReply = await handleRecordsWrite({ tenant, message: newWrite.message, messageStore, didResolver }); + const newWriteReply = await dwn.processMessage(tenant, newWrite.message); expect(newWriteReply.status.code).to.equal(202); @@ -324,7 +311,7 @@ describe('handleRecordsWrite()', () => { filter: { recordId: message.recordId } }); - const recordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const recordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(recordsQueryReply.status.code).to.equal(200); expect(recordsQueryReply.entries?.length).to.equal(1); expect((recordsQueryReply.entries![0] as RecordsWriteMessage).descriptor.published).to.equal(true); @@ -340,8 +327,8 @@ describe('handleRecordsWrite()', () => { const tenant = requester.did; // setting up a stub DID resolver - const didResolver = TestStubGenerator.createDidResolverStub(requester); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + TestStubGenerator.stubDidResolver(didResolver, [requester]); + const reply = await dwn.processMessage(tenant, message, dataStream); expect(reply.status.code).to.equal(202); @@ -352,9 +339,7 @@ describe('handleRecordsWrite()', () => { authorizationSignatureInput : Jws.createSignatureInput(requester) }); - const newWriteReply = await handleRecordsWrite({ - tenant, message: newWrite.message, messageStore, didResolver, dataStream: DataStream.fromBytes(newData) - }); + const newWriteReply = await dwn.processMessage(tenant, newWrite.message, DataStream.fromBytes(newData)); expect(newWriteReply.status.code).to.equal(202); @@ -364,7 +349,7 @@ describe('handleRecordsWrite()', () => { filter: { recordId: message.recordId } }); - const recordsQueryReply = await handleRecordsQuery({ tenant, message: recordsQueryMessageData.message, messageStore, didResolver }); + const recordsQueryReply = await dwn.processMessage(tenant, recordsQueryMessageData.message); expect(recordsQueryReply.status.code).to.equal(200); expect(recordsQueryReply.entries?.length).to.equal(1); @@ -383,8 +368,8 @@ describe('handleRecordsWrite()', () => { }); const tenant = requester.did; - const didResolver = TestStubGenerator.createDidResolverStub(requester); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + TestStubGenerator.stubDidResolver(didResolver, [requester]); + const reply = await dwn.processMessage(tenant, message, dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('initial write is not found'); @@ -397,8 +382,9 @@ describe('handleRecordsWrite()', () => { }); const tenant = requester.did; - const didResolver = TestStubGenerator.createDidResolverStub(requester); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + TestStubGenerator.stubDidResolver(didResolver, [requester]); + + const reply = await dwn.processMessage(tenant, message, dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('must match dateCreated'); @@ -406,14 +392,13 @@ describe('handleRecordsWrite()', () => { it('should return 400 if `contextId` in an initial protocol-base write mismatches with the expected deterministic `contextId`', async () => { // generate a message with protocol so that computed contextId is also computed and included in message - const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ protocol: 'anyValue' }); + const { message, dataStream, requester } = await TestDataGenerator.generateRecordsWrite({ protocol: 'anyValue' }); message.contextId = await TestDataGenerator.randomCborSha256Cid(); // make contextId mismatch from computed value - const didResolver = sinon.createStubInstance(DidResolver); - const messageStore = sinon.createStubInstance(MessageStoreLevel); + TestStubGenerator.stubDidResolver(didResolver, [requester]); - const reply = await handleRecordsWrite({ tenant: 'unused-tenant-DID', message, messageStore, didResolver, dataStream }); + const reply = await dwn.processMessage('unused-tenant-DID', message, dataStream); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('does not match deterministic contextId'); }); @@ -444,6 +429,7 @@ describe('handleRecordsWrite()', () => { } }; const alice = await TestDataGenerator.generatePersona(); + const bob = await TestDataGenerator.generatePersona(); const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ requester: alice, @@ -451,16 +437,13 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - // setting up a stub did resolver - const aliceDidResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, bob]); - const protocolsConfigureReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolsConfig.message, messageStore, didResolver: aliceDidResolver, dataStream: protocolsConfig.dataStream - }); + const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); expect(protocolsConfigureReply.status.code).to.equal(202); // generate a `RecordsWrite` message from bob allowed by anyone - const bob = await TestDataGenerator.generatePersona(); const bobData = new TextEncoder().encode('data from bob'); const emailFromBob = await TestDataGenerator.generateRecordsWrite( { @@ -471,11 +454,7 @@ describe('handleRecordsWrite()', () => { } ); - const bobDidResolver = TestStubGenerator.createDidResolverStub(bob); - - const bobWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: emailFromBob.message, messageStore, didResolver: bobDidResolver, dataStream: emailFromBob.dataStream - }); + const bobWriteReply = await dwn.processMessage(alice.did, emailFromBob.message, emailFromBob.dataStream); expect(bobWriteReply.status.code).to.equal(202); // verify bob's message got written to the DB @@ -483,9 +462,7 @@ describe('handleRecordsWrite()', () => { requester : alice, filter : { recordId: emailFromBob.message.recordId } }); - const bobRecordQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: messageDataForQueryingBobsWrite.message, messageStore, didResolver: aliceDidResolver - }); + const bobRecordQueryReply = await dwn.processMessage(alice.did, messageDataForQueryingBobsWrite.message); expect(bobRecordQueryReply.status.code).to.equal(200); expect(bobRecordQueryReply.entries?.length).to.equal(1); expect((bobRecordQueryReply.entries![0] as any).encodedData).to.equal(base64url.baseEncode(bobData)); @@ -500,6 +477,7 @@ describe('handleRecordsWrite()', () => { const credentialResponseSchema = protocolDefinition.labels.credentialResponse.schema; const alice = await TestDataGenerator.generatePersona(); + const vcIssuer = await TestDataGenerator.generatePersona(); const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ requester: alice, @@ -507,16 +485,13 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - // setting up a stub did resolver - const aliceDidResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, vcIssuer]); - const protocolWriteReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolsConfig.message, messageStore, didResolver: aliceDidResolver, dataStream: protocolsConfig.dataStream - }); + const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); expect(protocolWriteReply.status.code).to.equal(202); // write a credential application to Alice's DWN to simulate that she has sent a credential application to a VC issuer - const vcIssuer = await TestDataGenerator.generatePersona(); const encodedCredentialApplication = new TextEncoder().encode('credential application data'); const credentialApplication = await TestDataGenerator.generateRecordsWrite({ requester : alice, @@ -527,13 +502,7 @@ describe('handleRecordsWrite()', () => { }); const credentialApplicationContextId = await credentialApplication.recordsWrite.getEntryId(); - const credentialApplicationReply = await handleRecordsWrite({ - tenant : alice.did, - message : credentialApplication.message, - messageStore, - didResolver : aliceDidResolver, - dataStream : credentialApplication.dataStream - }); + const credentialApplicationReply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream); expect(credentialApplicationReply.status.code).to.equal(202); // generate a credential application response message from VC issuer @@ -550,15 +519,7 @@ describe('handleRecordsWrite()', () => { } ); - const vcIssuerDidResolver = TestStubGenerator.createDidResolverStub(vcIssuer); - - const credentialResponseReply = await handleRecordsWrite({ - tenant : alice.did, - message : credentialResponse.message, - messageStore, - didResolver : vcIssuerDidResolver, - dataStream : credentialResponse.dataStream - }); + const credentialResponseReply = await dwn.processMessage(alice.did, credentialResponse.message, credentialResponse.dataStream); expect(credentialResponseReply.status.code).to.equal(202); // verify VC issuer's message got written to the DB @@ -566,12 +527,7 @@ describe('handleRecordsWrite()', () => { requester : alice, filter : { recordId: credentialResponse.message.recordId } }); - const applicationResponseQueryReply = await handleRecordsQuery({ - tenant : alice.did, - message : messageDataForQueryingCredentialResponse.message, - messageStore, - didResolver : aliceDidResolver - }); + const applicationResponseQueryReply = await dwn.processMessage(alice.did, messageDataForQueryingCredentialResponse.message); expect(applicationResponseQueryReply.status.code).to.equal(200); expect(applicationResponseQueryReply.entries?.length).to.equal(1); expect((applicationResponseQueryReply.entries![0] as any).encodedData) @@ -602,6 +558,7 @@ describe('handleRecordsWrite()', () => { } }; const alice = await TestDataGenerator.generatePersona(); + const bob = await TestDataGenerator.generatePersona(); const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ requester: alice, @@ -609,16 +566,13 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - // setting up a stub did resolver - const aliceDidResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, bob]); - const protocolWriteReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolsConfig.message, messageStore, didResolver: aliceDidResolver, dataStream: protocolsConfig.dataStream - }); + const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); expect(protocolWriteReply.status.code).to.equal(202); // generate a `RecordsWrite` message from bob - const bob = await TestDataGenerator.generatePersona(); const bobData = new TextEncoder().encode('data from bob'); const notesFromBob = await TestDataGenerator.generateRecordsWrite( { @@ -629,11 +583,7 @@ describe('handleRecordsWrite()', () => { } ); - const bobDidResolver = TestStubGenerator.createDidResolverStub(bob); - - const bobWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: notesFromBob.message, messageStore, didResolver: bobDidResolver, dataStream: notesFromBob.dataStream - }); + const bobWriteReply = await dwn.processMessage(alice.did, notesFromBob.message, notesFromBob.dataStream); expect(bobWriteReply.status.code).to.equal(202); // verify bob's message got written to the DB @@ -641,9 +591,7 @@ describe('handleRecordsWrite()', () => { requester : alice, filter : { recordId: notesFromBob.message.recordId } }); - const bobRecordQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: messageDataForQueryingBobsWrite.message, messageStore, didResolver: aliceDidResolver - }); + const bobRecordQueryReply = await dwn.processMessage(alice.did, messageDataForQueryingBobsWrite.message); expect(bobRecordQueryReply.status.code).to.equal(200); expect(bobRecordQueryReply.entries?.length).to.equal(1); expect((bobRecordQueryReply.entries![0] as any).encodedData).to.equal(base64url.baseEncode(bobData)); @@ -656,15 +604,11 @@ describe('handleRecordsWrite()', () => { data : newNotesBytes }); - const newWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: newNotesFromBob.message, messageStore, didResolver: bobDidResolver, dataStream: newNotesFromBob.dataStream - }); + const newWriteReply = await dwn.processMessage(alice.did, newNotesFromBob.message, newNotesFromBob.dataStream); expect(newWriteReply.status.code).to.equal(202); // verify bob's message got written to the DB - const newRecordQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: messageDataForQueryingBobsWrite.message, messageStore, didResolver: aliceDidResolver - }); + const newRecordQueryReply = await dwn.processMessage(alice.did, messageDataForQueryingBobsWrite.message); expect(newRecordQueryReply.status.code).to.equal(200); expect(newRecordQueryReply.entries?.length).to.equal(1); expect((newRecordQueryReply.entries![0] as any).encodedData).to.equal(Encoder.bytesToBase64Url(newNotesBytes)); @@ -694,6 +638,8 @@ describe('handleRecordsWrite()', () => { } }; const alice = await TestDataGenerator.generatePersona(); + const bob = await TestDataGenerator.generatePersona(); + const carol = await TestDataGenerator.generatePersona(); const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ requester: alice, @@ -701,16 +647,13 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - // setting up a stub did resolver - const aliceDidResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, bob, carol]); - const protocolWriteReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolsConfig.message, messageStore, didResolver: aliceDidResolver, dataStream: protocolsConfig.dataStream - }); + const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); expect(protocolWriteReply.status.code).to.equal(202); // generate a `RecordsWrite` message from bob - const bob = await TestDataGenerator.generatePersona(); const bobData = new TextEncoder().encode('data from bob'); const notesFromBob = await TestDataGenerator.generateRecordsWrite( { @@ -721,11 +664,7 @@ describe('handleRecordsWrite()', () => { } ); - const bobDidResolver = TestStubGenerator.createDidResolverStub(bob); - - const bobWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: notesFromBob.message, messageStore, didResolver: bobDidResolver, dataStream: notesFromBob.dataStream - }); + const bobWriteReply = await dwn.processMessage(alice.did, notesFromBob.message, notesFromBob.dataStream); expect(bobWriteReply.status.code).to.equal(202); // verify bob's message got written to the DB @@ -733,15 +672,12 @@ describe('handleRecordsWrite()', () => { requester : alice, filter : { recordId: notesFromBob.message.recordId } }); - const bobRecordQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: messageDataForQueryingBobsWrite.message, messageStore, didResolver: aliceDidResolver - }); + const bobRecordQueryReply = await dwn.processMessage(alice.did, messageDataForQueryingBobsWrite.message); expect(bobRecordQueryReply.status.code).to.equal(200); expect(bobRecordQueryReply.entries?.length).to.equal(1); expect((bobRecordQueryReply.entries![0] as any).encodedData).to.equal(base64url.baseEncode(bobData)); // generate a new message from carol updating the existing notes, which should not be allowed/accepted - const carol = await TestDataGenerator.generatePersona(); const newNotesData = new TextEncoder().encode('different data by carol'); const newNotesFromBob = await TestDataGenerator.generateRecordsWrite( { @@ -753,10 +689,7 @@ describe('handleRecordsWrite()', () => { } ); - const carolDidResolver = TestStubGenerator.createDidResolverStub(carol); - const carolWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: newNotesFromBob.message, messageStore, didResolver: carolDidResolver, dataStream: newNotesFromBob.dataStream - }); + const carolWriteReply = await dwn.processMessage(alice.did, newNotesFromBob.message, newNotesFromBob.dataStream); expect(carolWriteReply.status.code).to.equal(401); expect(carolWriteReply.status.detail).to.contain('must match to author of initial write'); }); @@ -788,6 +721,7 @@ describe('handleRecordsWrite()', () => { } }; const alice = await TestDataGenerator.generatePersona(); + const bob = await TestDataGenerator.generatePersona(); const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ requester: alice, @@ -795,16 +729,13 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - // setting up a stub did resolver - const aliceDidResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, bob]); - const protocolWriteReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolsConfig.message, messageStore, didResolver: aliceDidResolver, dataStream: protocolsConfig.dataStream - }); + const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); expect(protocolWriteReply.status.code).to.equal(202); // generate a `RecordsWrite` message from bob - const bob = await TestDataGenerator.generatePersona(); const bobData = new TextEncoder().encode('data from bob'); const notesFromBob = await TestDataGenerator.generateRecordsWrite( { @@ -815,11 +746,7 @@ describe('handleRecordsWrite()', () => { } ); - const bobDidResolver = TestStubGenerator.createDidResolverStub(bob); - - const bobWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: notesFromBob.message, messageStore, didResolver: bobDidResolver, dataStream: notesFromBob.dataStream - }); + const bobWriteReply = await dwn.processMessage(alice.did, notesFromBob.message, notesFromBob.dataStream); expect(bobWriteReply.status.code).to.equal(202); // verify bob's message got written to the DB @@ -827,9 +754,7 @@ describe('handleRecordsWrite()', () => { requester : alice, filter : { recordId: notesFromBob.message.recordId } }); - const bobRecordQueryReply = await handleRecordsQuery({ - tenant: alice.did, message: messageDataForQueryingBobsWrite.message, messageStore, didResolver: aliceDidResolver - }); + const bobRecordQueryReply = await dwn.processMessage(alice.did, messageDataForQueryingBobsWrite.message); expect(bobRecordQueryReply.status.code).to.equal(200); expect(bobRecordQueryReply.entries?.length).to.equal(1); expect((bobRecordQueryReply.entries![0] as any).encodedData).to.equal(base64url.baseEncode(bobData)); @@ -847,9 +772,7 @@ describe('handleRecordsWrite()', () => { } ); - const newWriteReply = await handleRecordsWrite({ - tenant: alice.did, message: newNotesFromBob.message, messageStore, didResolver: bobDidResolver, dataStream: newNotesFromBob.dataStream - }); + const newWriteReply = await dwn.processMessage(alice.did, newNotesFromBob.message, newNotesFromBob.dataStream); expect(newWriteReply.status.code).to.equal(400); expect(newWriteReply.status.detail).to.contain('recipient is an immutable property'); }); @@ -864,6 +787,7 @@ describe('handleRecordsWrite()', () => { const credentialResponseSchema = protocolDefinition.labels.credentialResponse.schema; const alice = await TestDataGenerator.generatePersona(); + const fakeVcIssuer = await TestDataGenerator.generatePersona(); const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ requester: alice, @@ -871,12 +795,10 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - // setting up a stub did resolver - const aliceDidResolver = TestStubGenerator.createDidResolverStub(alice); + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, fakeVcIssuer]); - const protocolWriteReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolsConfig.message, messageStore, didResolver: aliceDidResolver, dataStream: protocolsConfig.dataStream - }); + const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); expect(protocolWriteReply.status.code).to.equal(202); // write a credential application to Alice's DWN to simulate that she has sent a credential application to a VC issuer @@ -891,17 +813,10 @@ describe('handleRecordsWrite()', () => { }); const credentialApplicationContextId = await credentialApplication.recordsWrite.getEntryId(); - const credentialApplicationReply = await handleRecordsWrite({ - tenant : alice.did, - message : credentialApplication.message, - messageStore, - didResolver : aliceDidResolver, - dataStream : credentialApplication.dataStream - }); + const credentialApplicationReply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream); expect(credentialApplicationReply.status.code).to.equal(202); // generate a credential application response message from a fake VC issuer - const fakeVcIssuer = await TestDataGenerator.generatePersona(); const encodedCredentialResponse = new TextEncoder().encode('credential response data'); const credentialResponse = await TestDataGenerator.generateRecordsWrite( { @@ -915,15 +830,7 @@ describe('handleRecordsWrite()', () => { } ); - const vcIssuerDidResolver = TestStubGenerator.createDidResolverStub(fakeVcIssuer); - - const credentialResponseReply = await handleRecordsWrite({ - tenant : alice.did, - message : credentialResponse.message, - messageStore, - didResolver : vcIssuerDidResolver, - dataStream : credentialResponse.dataStream - }); + const credentialResponseReply = await dwn.processMessage(alice.did, credentialResponse.message, credentialResponse.dataStream); expect(credentialResponseReply.status.code).to.equal(401); expect(credentialResponseReply.status.detail).to.contain('unexpected inbound message author'); }); @@ -939,9 +846,7 @@ describe('handleRecordsWrite()', () => { data }); - const reply = await handleRecordsWrite({ - tenant: alice.did, message: credentialApplication.message, messageStore, didResolver, dataStream: credentialApplication.dataStream - }); + const reply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('unable to find protocol definition'); }); @@ -956,9 +861,7 @@ describe('handleRecordsWrite()', () => { protocolDefinition : credentialIssuanceProtocolDefinition }); - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); const data = Encoder.stringToBytes('any data'); @@ -970,9 +873,7 @@ describe('handleRecordsWrite()', () => { data }); - const reply = await handleRecordsWrite({ - tenant: alice.did, message: credentialApplication.message, messageStore, didResolver, dataStream: credentialApplication.dataStream - }); + const reply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.equal('record with schema \'unexpectedSchema\' not allowed in protocol'); }); @@ -989,9 +890,7 @@ describe('handleRecordsWrite()', () => { }); const credentialResponseSchema = protocolDefinition.labels.credentialResponse.schema; - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); const data = Encoder.stringToBytes('any data'); @@ -1003,9 +902,7 @@ describe('handleRecordsWrite()', () => { data }); - const reply = await handleRecordsWrite({ - tenant: alice.did, message: credentialApplication.message, messageStore, didResolver, dataStream: credentialApplication.dataStream - }); + const reply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('not allowed in structure level'); }); @@ -1031,9 +928,7 @@ describe('handleRecordsWrite()', () => { protocolDefinition }); - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); // test that Alice is allowed to write to her own DWN @@ -1046,9 +941,7 @@ describe('handleRecordsWrite()', () => { data }); - let reply = await handleRecordsWrite({ - tenant: alice.did, message: aliceWriteMessageData.message, messageStore, didResolver, dataStream: aliceWriteMessageData.dataStream - }); + let reply = await dwn.processMessage(alice.did, aliceWriteMessageData.message, aliceWriteMessageData.dataStream); expect(reply.status.code).to.equal(202); // test that Bob is not allowed to write to Alice's DWN @@ -1061,9 +954,7 @@ describe('handleRecordsWrite()', () => { data }); - reply = await handleRecordsWrite({ - tenant: alice.did, message: bobWriteMessageData.message, messageStore, didResolver, dataStream: bobWriteMessageData.dataStream - }); + reply = await dwn.processMessage(alice.did, bobWriteMessageData.message, bobWriteMessageData.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('no allow rule defined for requester'); }); @@ -1085,9 +976,7 @@ describe('handleRecordsWrite()', () => { protocolDefinition : invalidProtocolDefinition }); - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); // simulate Alice's VC applications with both issuer @@ -1101,9 +990,7 @@ describe('handleRecordsWrite()', () => { }); const contextId = await messageDataWithIssuerA.recordsWrite.getEntryId(); - let reply = await handleRecordsWrite({ - tenant: alice.did, message: messageDataWithIssuerA.message, messageStore, didResolver, dataStream: messageDataWithIssuerA.dataStream - }); + let reply = await dwn.processMessage(alice.did, messageDataWithIssuerA.message, messageDataWithIssuerA.dataStream); expect(reply.status.code).to.equal(202); // simulate issuer attempting to respond to Alice's VC application @@ -1117,9 +1004,7 @@ describe('handleRecordsWrite()', () => { data }); - reply = await handleRecordsWrite({ - tenant: alice.did, message: invalidResponseByIssuerA.message, messageStore, didResolver, dataStream: invalidResponseByIssuerA.dataStream - }); + reply = await dwn.processMessage(alice.did, invalidResponseByIssuerA.message, invalidResponseByIssuerA.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('path to expected recipient is longer than actual length of ancestor message chain'); }); @@ -1141,9 +1026,7 @@ describe('handleRecordsWrite()', () => { protocolDefinition : invalidProtocolDefinition }); - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: alice.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); // simulate Alice's VC application to an issuer @@ -1157,9 +1040,7 @@ describe('handleRecordsWrite()', () => { }); const contextId = await messageDataWithIssuerA.recordsWrite.getEntryId(); - let reply = await handleRecordsWrite({ - tenant: alice.did, message: messageDataWithIssuerA.message, messageStore, didResolver, dataStream: messageDataWithIssuerA.dataStream - }); + let reply = await dwn.processMessage(alice.did, messageDataWithIssuerA.message, messageDataWithIssuerA.dataStream); expect(reply.status.code).to.equal(202); // simulate issuer attempting to respond to Alice's VC application @@ -1173,9 +1054,7 @@ describe('handleRecordsWrite()', () => { data }); - reply = await handleRecordsWrite({ - tenant: alice.did, message: invalidResponseByIssuerA.message, messageStore, didResolver, dataStream: invalidResponseByIssuerA.dataStream - }); + reply = await dwn.processMessage(alice.did, invalidResponseByIssuerA.message, invalidResponseByIssuerA.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('mismatching record schema'); }); @@ -1198,9 +1077,7 @@ describe('handleRecordsWrite()', () => { protocolDefinition : protocolDefinition }); - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: pfi.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(pfi.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); // simulate Alice's ask and PFI's offer already occurred @@ -1214,9 +1091,7 @@ describe('handleRecordsWrite()', () => { }); const contextId = await askMessageData.recordsWrite.getEntryId(); - let reply = await handleRecordsWrite({ - tenant: pfi.did, message: askMessageData.message, messageStore, didResolver, dataStream: askMessageData.dataStream - }); + let reply = await dwn.processMessage(pfi.did, askMessageData.message, askMessageData.dataStream); expect(reply.status.code).to.equal(202); const offerMessageData = await TestDataGenerator.generateRecordsWrite({ @@ -1229,9 +1104,7 @@ describe('handleRecordsWrite()', () => { data }); - reply = await handleRecordsWrite({ - tenant: pfi.did, message: offerMessageData.message, messageStore, didResolver, dataStream: offerMessageData.dataStream - }); + reply = await dwn.processMessage(pfi.did, offerMessageData.message, offerMessageData.dataStream); expect(reply.status.code).to.equal(202); // the actual test: making sure fulfillment message is accepted @@ -1244,9 +1117,7 @@ describe('handleRecordsWrite()', () => { protocol, data }); - reply = await handleRecordsWrite({ - tenant: pfi.did, message: fulfillmentMessageData.message, messageStore, didResolver, dataStream: fulfillmentMessageData.dataStream - }); + reply = await dwn.processMessage(pfi.did, fulfillmentMessageData.message, fulfillmentMessageData.dataStream); expect(reply.status.code).to.equal(202); // verify the fulfillment message is stored @@ -1256,8 +1127,8 @@ describe('handleRecordsWrite()', () => { }); // verify the data is written - const recordsQueryReply = await handleRecordsQuery({ - tenant: pfi.did, message: recordsQueryMessageData.message, messageStore, didResolver }); + const recordsQueryReply = await dwn.processMessage( + pfi.did, recordsQueryMessageData.message); expect(recordsQueryReply.status.code).to.equal(200); expect(recordsQueryReply.entries?.length).to.equal(1); expect((recordsQueryReply.entries![0] as RecordsWriteMessage).descriptor.dataCid) @@ -1283,9 +1154,7 @@ describe('handleRecordsWrite()', () => { protocolDefinition : protocolDefinition }); - const protocolConfigureReply = await handleProtocolsConfigure({ - tenant: pfi.did, message: protocolConfig.message, messageStore, didResolver, dataStream: protocolConfig.dataStream - }); + const protocolConfigureReply = await dwn.processMessage(pfi.did, protocolConfig.message, protocolConfig.dataStream); expect(protocolConfigureReply.status.code).to.equal(202); // simulate Alice's ask @@ -1299,9 +1168,7 @@ describe('handleRecordsWrite()', () => { }); const contextId = await askMessageData.recordsWrite.getEntryId(); - let reply = await handleRecordsWrite({ - tenant: pfi.did, message: askMessageData.message, messageStore, didResolver, dataStream: askMessageData.dataStream - }); + let reply = await dwn.processMessage(pfi.did, askMessageData.message, askMessageData.dataStream); expect(reply.status.code).to.equal(202); // the actual test: making sure fulfillment message fails @@ -1314,9 +1181,7 @@ describe('handleRecordsWrite()', () => { protocol, data }); - reply = await handleRecordsWrite({ - tenant: pfi.did, message: fulfillmentMessageData.message, messageStore, didResolver, dataStream: fulfillmentMessageData.dataStream - }); + reply = await dwn.processMessage(pfi.did, fulfillmentMessageData.message, fulfillmentMessageData.dataStream); expect(reply.status.code).to.equal(401); expect(reply.status.detail).to.contain('no parent found'); }); @@ -1338,7 +1203,10 @@ describe('handleRecordsWrite()', () => { const tenant = requester.did; const didResolver = TestStubGenerator.createDidResolverStub(requester); const messageStore = sinon.createStubInstance(MessageStoreLevel); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + const dataStore = sinon.createStubInstance(DataStoreLevel); + + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const reply = await recordsWriteHandler.handle({ tenant, message, dataStream }); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('does not match recordId in authorization'); @@ -1359,7 +1227,10 @@ describe('handleRecordsWrite()', () => { const tenant = requester.did; const didResolver = sinon.createStubInstance(DidResolver); const messageStore = sinon.createStubInstance(MessageStoreLevel); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + const dataStore = sinon.createStubInstance(DataStoreLevel); + + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const reply = await recordsWriteHandler.handle({ tenant, message, dataStream }); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain('does not match contextId in authorization'); @@ -1367,16 +1238,17 @@ describe('handleRecordsWrite()', () => { it('should return 401 if `authorization` signature check fails', async () => { const { requester, message, dataStream } = await TestDataGenerator.generateRecordsWrite(); + const tenant = requester.did; - // setting up a stub did resolver & message store + // setting up a stub DID resolver & message store // intentionally not supplying the public key so a different public key is generated to simulate invalid signature const mismatchingPersona = await TestDataGenerator.generatePersona({ did: requester.did, keyId: requester.keyId }); const didResolver = TestStubGenerator.createDidResolverStub(mismatchingPersona); - const messageStore = sinon.createStubInstance(MessageStoreLevel); - const tenant = requester.did; + const dataStore = sinon.createStubInstance(DataStoreLevel); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const reply = await recordsWriteHandler.handle({ tenant, message, dataStream }); expect(reply.status.code).to.equal(401); }); @@ -1385,12 +1257,15 @@ describe('handleRecordsWrite()', () => { const requester = await TestDataGenerator.generatePersona(); const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ requester }); - // setting up a stub did resolver & message store + // setting up a stub DID resolver & message store const didResolver = TestStubGenerator.createDidResolverStub(requester); const messageStore = sinon.createStubInstance(MessageStoreLevel); + const dataStore = sinon.createStubInstance(DataStoreLevel); + + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); - const tenant = await (await TestDataGenerator.generatePersona()).did; - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + const tenant = await (await TestDataGenerator.generatePersona()).did; // unauthorized tenant + const reply = await recordsWriteHandler.handle({ tenant, message, dataStream }); expect(reply.status.code).to.equal(401); }); @@ -1419,7 +1294,10 @@ describe('handleRecordsWrite()', () => { const didResolver = TestStubGenerator.createDidResolverStub(requester); const messageStore = sinon.createStubInstance(MessageStoreLevel); - const reply = await handleRecordsWrite({ tenant, message, messageStore, didResolver, dataStream }); + const dataStore = sinon.createStubInstance(DataStoreLevel); + + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const reply = await recordsWriteHandler.handle({ tenant, message, dataStream }); expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain(`Only 'descriptorCid' is allowed in attestation payload`); @@ -1430,7 +1308,9 @@ describe('handleRecordsWrite()', () => { const bob = await DidKeyResolver.generate(); const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [alice, bob] }); - const writeReply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver, dataStream }); + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const writeReply = await recordsWriteHandler.handle({ tenant: alice.did, message, dataStream }); + expect(writeReply.status.code).to.equal(400); expect(writeReply.status.detail).to.contain('implementation only supports 1 attester'); }); @@ -1443,7 +1323,9 @@ describe('handleRecordsWrite()', () => { const anotherWrite = await TestDataGenerator.generateRecordsWrite({ attesters: [alice] }); message.attestation = anotherWrite.message.attestation; - const writeReply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver, dataStream }); + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const writeReply = await recordsWriteHandler.handle({ tenant: alice.did, message, dataStream }); + expect(writeReply.status.code).to.equal(400); expect(writeReply.status.detail).to.contain('does not match expected descriptorCid'); }); @@ -1458,23 +1340,31 @@ describe('handleRecordsWrite()', () => { const attestationNotReferencedByAuthorization = await RecordsWrite['createAttestation'](descriptorCid, Jws.createSignatureInputs([bob])); message.attestation = attestationNotReferencedByAuthorization; - const writeReply = await handleRecordsWrite({ tenant: alice.did, message, messageStore, didResolver, dataStream }); + const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore); + const writeReply = await recordsWriteHandler.handle({ tenant: alice.did, message, dataStream }); + expect(writeReply.status.code).to.equal(400); expect(writeReply.status.detail).to.contain('does not match attestationCid'); }); }); - it('should throw if `messageStore.put()` throws unknown error', async () => { + it('should throw if `storageController.put()` throws unknown error', async () => { const { requester, message, dataStream } = await TestDataGenerator.generateRecordsWrite(); + const tenant = requester.did; const didResolverStub = TestStubGenerator.createDidResolverStub(requester); const messageStoreStub = sinon.createStubInstance(MessageStoreLevel); messageStoreStub.query.resolves([]); - messageStoreStub.put.throws(new Error('an unknown error in messageStore.put()')); - const tenant = requester.did; - const handlerPromise = handleRecordsWrite({ tenant, message, messageStore: messageStoreStub, didResolver: didResolverStub, dataStream }); + // simulate throwing unexpected error + sinon.stub(StorageController, 'put').throws(new Error('an unknown error in messageStore.put()')); + + const dataStoreStub = sinon.createStubInstance(DataStoreLevel); + + const recordsWriteHandler = new RecordsWriteHandler(didResolverStub, messageStoreStub, dataStoreStub); + + const handlerPromise = recordsWriteHandler.handle({ tenant, message, dataStream }); await expect(handlerPromise).to.be.rejectedWith('an unknown error in messageStore.put()'); }); }); diff --git a/tests/store/message-store.spec.ts b/tests/store/message-store.spec.ts index 54c857dcd..cc4b2d5bd 100644 --- a/tests/store/message-store.spec.ts +++ b/tests/store/message-store.spec.ts @@ -90,10 +90,10 @@ describe('MessageStoreLevel Tests', () => { it('#170 - should be able to update (delete and insert new) indexes to an existing message', async () => { const alice = await DidKeyResolver.generate(); - const { message, dataStream } = await TestDataGenerator.generateRecordsWrite(); + const { message } = await TestDataGenerator.generateRecordsWrite(); // inserting the message indicating it is the 'latest' in the index - await messageStore.put(message, { tenant: alice.did, latest: 'true' }, dataStream); + await messageStore.put(message, { tenant: alice.did, latest: 'true' }); const results1 = await messageStore.query({ tenant: alice.did, latest: 'true' }); expect(results1.length).to.equal(1); @@ -115,9 +115,9 @@ describe('MessageStoreLevel Tests', () => { it('should index properties with characters beyond just letters and digits', async () => { const schema = 'http://my-awesome-schema/awesomeness_schema#awesome-1?id=awesome_1'; - const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ schema }); + const { message } = await TestDataGenerator.generateRecordsWrite({ schema }); - await messageStore.put(message, { schema }, dataStream); + await messageStore.put(message, { schema }); const results = await messageStore.query({ schema }); expect((results[0] as RecordsWriteMessage).descriptor.schema).to.equal(schema); diff --git a/tests/utils/test-stub-generator.ts b/tests/utils/test-stub-generator.ts index e495e4a46..1853c7531 100644 --- a/tests/utils/test-stub-generator.ts +++ b/tests/utils/test-stub-generator.ts @@ -19,4 +19,29 @@ export class TestStubGenerator { return didResolverStub; } + + /** + * Stubs resolution results for the given personas. + */ + public static stubDidResolver(didResolver: DidResolver, personas: Persona[]): void { + const didToResolutionMap = new Map(); + + for (const persona of personas) { + const mockResolution = TestDataGenerator.createDidResolutionResult(persona); + + didToResolutionMap.set(persona.did, mockResolution); + } + + sinon.stub(didResolver, 'resolve').callsFake((did) => { + const mockResolution = didToResolutionMap.get(did); + + return new Promise((resolve, _reject) => { + if (mockResolution === undefined) { + throw new Error('unexpected DID'); + } + + resolve(mockResolution); + }); + }); + } } diff --git a/tsconfig.json b/tsconfig.json index 43cb7eb47..e7b8e8b76 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,9 @@ "esModuleInterop": true }, "typedocOptions": { - "entryPoints": ["src/index.ts"], + "entryPoints": [ + "src/index.ts" + ], "out": "documentation", "readme": "none", "excludePrivate": "true",