diff --git a/README.md b/README.md index 371bb88cb..09e9b286f 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-94.72%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.15%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.45%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.72%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-94.74%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.13%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.48%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.74%25-brightgreen.svg?style=flat) ## Introduction diff --git a/src/dwn.ts b/src/dwn.ts index bac63a1a5..57fca1808 100644 --- a/src/dwn.ts +++ b/src/dwn.ts @@ -93,29 +93,12 @@ export class Dwn { * @param tenant The tenant DID to route the given message to. */ public async processMessage(tenant: string, rawMessage: any, dataStream?: Readable): Promise { - const isTenant = await this.tenantGate.isTenant(tenant); - if (!isTenant) { - return new MessageReply({ - status: { code: 401, detail: `${tenant} is not a tenant` } - }); + const errorMessageReply = await this.preprocessingChecks(tenant, rawMessage); + if (errorMessageReply !== undefined) { + return errorMessageReply; } - const dwnInterface = rawMessage?.descriptor?.interface; - const dwnMethod = rawMessage?.descriptor?.method; - if (dwnInterface === undefined || dwnMethod === undefined) { - return new MessageReply({ - status: { code: 400, detail: `Both interface and method must be present, interface: ${dwnInterface}, method: ${dwnMethod}` } - }); - } - - try { - // consider to push this down to individual handlers - Message.validateJsonSchema(rawMessage); - } catch (error) { - return MessageReply.fromError(error, 400); - } - - const handlerKey = dwnInterface + dwnMethod; + const handlerKey = rawMessage.descriptor.interface + rawMessage.descriptor.method; const methodHandlerReply = await this.methodHandlers[handlerKey].handle({ tenant, message: rawMessage as BaseMessage, @@ -129,22 +112,50 @@ export class Dwn { * Handles a `RecordsRead` message. */ public async handleRecordsRead(tenant: string, message: RecordsReadMessage): Promise { - const reply = await this.processMessage(tenant, message); - return reply as RecordsReadReply; + const errorMessageReply = await this.preprocessingChecks(tenant, message, DwnInterfaceName.Records, DwnMethodName.Read); + if (errorMessageReply !== undefined) { + return errorMessageReply; + } + + const handler = new RecordsReadHandler(this.didResolver, this.messageStore, this.dataStore); + return handler.handle({ tenant, message }); } /** * Handles a `MessagesGet` message. */ public async handleMessagesGet(tenant: string, message: MessagesGetMessage): Promise { - const reply = await this.processMessage(tenant, message); - return reply as MessagesGetReply; + const errorMessageReply = await this.preprocessingChecks(tenant, message, DwnInterfaceName.Messages, DwnMethodName.Get); + if (errorMessageReply !== undefined) { + return errorMessageReply; + } + + const handler = new MessagesGetHandler(this.didResolver, this.messageStore, this.dataStore); + return handler.handle({ tenant, message }); } /** * Privileged method for writing a pruned initial `RecordsWrite` to a DWN without needing to supply associated data. */ public async synchronizePrunedInitialRecordsWrite(tenant: string, message: RecordsWriteMessage): Promise { + const errorMessageReply = await this.preprocessingChecks(tenant, message, DwnInterfaceName.Records, DwnMethodName.Write); + if (errorMessageReply !== undefined) { + return errorMessageReply; + } + + const methodHandlerReply = await this.prunedInitialRecordsWriteHandler.handle({ tenant, message }); + return methodHandlerReply; + } + + /** + * Common checks for handlers. + */ + private async preprocessingChecks( + tenant: string, + rawMessage: any, + expectedInterface?: DwnInterfaceName, + expectedMethod?: DwnMethodName + ): Promise { const isTenant = await this.tenantGate.isTenant(tenant); if (!isTenant) { return new MessageReply({ @@ -152,27 +163,34 @@ export class Dwn { }); } - // DWN interface and method check mainly for pure JS - const dwnInterface = message?.descriptor?.interface; - const dwnMethod = message?.descriptor?.method; - if (dwnInterface !== DwnInterfaceName.Records || dwnMethod !== DwnMethodName.Write) { + // Verify interface and method + const dwnInterface = rawMessage?.descriptor?.interface; + const dwnMethod = rawMessage?.descriptor?.method; + if (dwnInterface === undefined || dwnMethod === undefined) { + return new MessageReply({ + status: { code: 400, detail: `Both interface and method must be present, interface: ${dwnInterface}, method: ${dwnMethod}` } + }); + } + if (expectedInterface !== undefined && expectedInterface !== dwnInterface) { return new MessageReply({ - status: { - code : 400, - detail : `Invalid DWN interface or method: expecting ${DwnInterfaceName.Records}${DwnMethodName.Write}, got ${dwnInterface}${dwnMethod}.` - } + status: { code: 400, detail: `Expected interface ${expectedInterface}, received ${dwnInterface}` } + }); + } + if (expectedMethod !== undefined && expectedMethod !== dwnMethod) { + return new MessageReply({ + status: { code: 400, detail: `Expected method ${expectedInterface}${expectedMethod}, received ${dwnInterface}${dwnMethod}` } }); } + // validate message structure try { // consider to push this down to individual handlers - Message.validateJsonSchema(message); + Message.validateJsonSchema(rawMessage); } catch (error) { return MessageReply.fromError(error, 400); } - const methodHandlerReply = await this.prunedInitialRecordsWriteHandler.handle({ tenant, message }); - return methodHandlerReply; + return undefined; } public async dump(): Promise { diff --git a/tests/dwn.spec.ts b/tests/dwn.spec.ts index 8ec910e2e..3b76a8256 100644 --- a/tests/dwn.spec.ts +++ b/tests/dwn.spec.ts @@ -11,7 +11,8 @@ import { Encoder } from '../src/index.js'; import { EventLogLevel } from '../src/event-log/event-log-level.js'; import { MessageStoreLevel } from '../src/store/message-store-level.js'; import { TestDataGenerator } from './utils/test-data-generator.js'; -import { DwnInterfaceName, Message } from '../src/core/message.js'; +import { DwnInterfaceName, DwnMethodName, Message } from '../src/core/message.js'; +import { Jws, RecordsRead } from '../src/index.js'; chai.use(chaiAsPromised); @@ -165,8 +166,24 @@ describe('DWN', () => { }); }); + describe('handleRecordsRead', () => { + it('should return error if preprocessing checks fail', async () => { + const alice = await DidKeyResolver.generate(); + + const recordsRead = await RecordsRead.create({ + recordId : 'recordId-doesnt-matter', + authorizationSignatureInput : Jws.createSignatureInput(alice) + }); + (recordsRead.message as any).descriptor.method = 'Write'; // Will cause interface and method check to fail + const reply = await dwn.handleRecordsRead(alice.did, recordsRead.message); + + expect(reply.status.code).to.not.equal(200); + }); + }); + describe('handleMessagesGet', () => { - it('increases test coverage :)', async () => { + // increases test coverage :) + it('runs successfully', async () => { const did = await DidKeyResolver.generate(); const alice = await TestDataGenerator.generatePersona(did); const messageCids: string[] = []; @@ -199,6 +216,24 @@ describe('DWN', () => { expect(messageReply.messageCid).to.equal(cid); } }); + + it('should return error if preprocessing checks fail', async () => { + const alice = await DidKeyResolver.generate(); + + const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ + requester: alice + }); + + const messageCids = [await Message.getCid(recordsWrite.message)]; + const { messagesGet } = await TestDataGenerator.generateMessagesGet({ + requester: alice, + messageCids + }); + (messagesGet.message as any).descriptor.interface = 'Protocols'; // Will cause interface and method check to fail + const reply = await dwn.handleMessagesGet(alice.did, messagesGet.message); + + expect(reply.status.code).to.not.equal(200); + }); }); describe('synchronizePrunedInitialRecordsWrite()', () => { @@ -294,21 +329,21 @@ describe('DWN', () => { const alice = await DidKeyResolver.generate(); const reply1 = await dwn.synchronizePrunedInitialRecordsWrite(alice.did, undefined as unknown as RecordsWriteMessage ); // missing message expect(reply1.status.code).to.equal(400); - expect(reply1.status.detail).to.contain('Invalid DWN interface or method'); + expect(reply1.status.detail).to.contain('Both interface and method must be present'); const reply2 = await dwn.synchronizePrunedInitialRecordsWrite( alice.did, - { descriptor: { interface: 'IncorrectInterface' } } as RecordsWriteMessage + { descriptor: { interface: 'IncorrectInterface', method: DwnMethodName.Write } } as RecordsWriteMessage ); expect(reply2.status.code).to.equal(400); - expect(reply2.status.detail).to.contain('Invalid DWN interface or method'); + expect(reply2.status.detail).to.contain(`Expected interface ${DwnInterfaceName.Records}`); const reply3 = await dwn.synchronizePrunedInitialRecordsWrite( alice.did, { descriptor: { interface: DwnInterfaceName.Records, method: 'IncorrectMethod' } } as RecordsWriteMessage ); expect(reply3.status.code).to.equal(400); - expect(reply3.status.detail).to.contain('Invalid DWN interface or method'); + expect(reply3.status.detail).to.contain(`Expected method ${DwnInterfaceName.Records}${DwnMethodName.Write}`); }); }); });