diff --git a/README.md b/README.md index edac3b807..34233ae44 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Here's to a thrilling Hacktoberfest voyage with us! 🎉 # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-98.35%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.14%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-95.73%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-98.35%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-98.46%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.29%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-95.73%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-98.46%25-brightgreen.svg?style=flat) - [Introduction](#introduction) - [Installation](#installation) diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 11c3996ba..cb88f2117 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -15,6 +15,10 @@ export class DwnError extends Error { export enum DwnErrorCode { AuthenticateJwsMissing = 'AuthenticateJwsMissing', AuthorizationUnknownAuthor = 'AuthorizationUnknownAuthor', + DidMethodNotSupported = 'DidMethodNotSupported', + DidNotString = 'DidNotString', + DidNotValid = 'DidNotValid', + DidResolutionFailed = 'DidResolutionFailed', GeneralJwsVerifierInvalidSignature = 'GeneralJwsVerifierInvalidSignature', GrantAuthorizationGrantExpired = 'GrantAuthorizationGrantExpired', GrantAuthorizationGrantMissing = 'GrantAuthorizationGrantMissing', diff --git a/src/did/did-resolver.ts b/src/did/did-resolver.ts index 27bd68ea6..944399504 100644 --- a/src/did/did-resolver.ts +++ b/src/did/did-resolver.ts @@ -5,6 +5,7 @@ import { Did } from './did.js'; import { DidIonResolver } from './did-ion-resolver.js'; import { DidKeyResolver } from './did-key-resolver.js'; import { MemoryCache } from '../utils/memory-cache.js'; +import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; /** * A DID resolver that by default supports `did:key` and `did:ion` DIDs. @@ -49,7 +50,7 @@ export class DidResolver { const didResolver = this.didResolvers.get(didMethod); if (!didResolver) { - throw new Error(`${didMethod} DID method not supported`); + throw new DwnError(DwnErrorCode.DidMethodNotSupported, `${didMethod} DID method not supported`); } // use cached result if exists @@ -66,7 +67,7 @@ export class DidResolver { let errMsg = `Failed to resolve DID ${did}.`; errMsg += error ? ` Error: ${error}` : ''; - throw new Error(errMsg); + throw new DwnError(DwnErrorCode.DidResolutionFailed, errMsg); } return resolutionResult; diff --git a/src/did/did.ts b/src/did/did.ts index aeff9cbcd..2bf69138f 100644 --- a/src/did/did.ts +++ b/src/did/did.ts @@ -1,3 +1,5 @@ +import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; + /** * DID related operations. */ @@ -16,13 +18,13 @@ export class Did { */ public static validate(did: unknown): void { if (typeof did !== 'string') { - throw new Error(`DID is not string: ${did}`); + throw new DwnError(DwnErrorCode.DidNotString, `DID is not string: ${did}`); } // eslint-disable-next-line const didRegex= /^did:([a-z0-9]+):((?:(?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))*:)*((?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))+))((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\/[^#?]*)?([?][^#]*)?(#.*)?$/; if (!didRegex.test(did)) { - throw new TypeError(`DID is not a valid DID: ${did}`); + throw new DwnError(DwnErrorCode.DidNotValid, `DID is not a valid DID: ${did}`); } } diff --git a/tests/did/did-resolver.spec.ts b/tests/did/did-resolver.spec.ts index 7301eb19b..e9cac319e 100644 --- a/tests/did/did-resolver.spec.ts +++ b/tests/did/did-resolver.spec.ts @@ -4,6 +4,7 @@ import chai, { expect } from 'chai'; import { DidIonResolver } from '../../src/did/did-ion-resolver.js'; import { DidResolver } from '../../src/did/did-resolver.js'; +import { DwnErrorCode } from '../../src/core/dwn-error.js'; // extends chai to test promises chai.use(chaiAsPromised); @@ -32,4 +33,63 @@ describe('DidResolver', () => { sinon.assert.calledTwice(cacheGetSpy); // should try to fetch from cache both times sinon.assert.calledOnce(ionDidResolveSpy); // should only resolve using ION resolver once (the first time) }); + + it('should throw error when invalid DID is used', async () => { + const did = 'blah'; + const didIonResolver = new DidIonResolver(); + const didResolver = new DidResolver([didIonResolver]); + + await expect(didResolver.resolve(did)).to.eventually.be.rejected.and.has.property('code', DwnErrorCode.DidNotValid); + }); + + it('should throw error when unsupported DID method is used', async () => { + const did = 'did:foo:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w'; + const didIonResolver = new DidIonResolver(); + const didResolver = new DidResolver([didIonResolver]); + + await expect(didResolver.resolve(did)).to.eventually.be.rejected.and.has.property('code', DwnErrorCode.DidMethodNotSupported); + }); + + it('should throw error when resolution fails due to error in didResolutionMetadata', async () => { + const did = 'did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w'; + const didIonResolver = new DidIonResolver('unusedResolutionEndpoint'); + const didResolver = new DidResolver([didIonResolver]); + + const mockResolution = { + didDocument : 'any' as any, + didResolutionMetadata : { error: 'some error' }, + didDocumentMetadata : 'any' as any + }; + + const ionDidResolveSpy = sinon.stub(didIonResolver, 'resolve').resolves(mockResolution); + const cacheGetSpy = sinon.spy(didResolver['cache'], 'get'); + + await expect(didResolver.resolve(did)).to.eventually.be.rejected.and.has.property('code', DwnErrorCode.DidResolutionFailed); + + sinon.assert.calledOnce(cacheGetSpy); + sinon.assert.calledOnce(ionDidResolveSpy); + + }); + + it('should throw error when resolution fails due to undefined didDocument', async () => { + const did = 'did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w'; + const didIonResolver = new DidIonResolver('unusedResolutionEndpoint'); + const didResolver = new DidResolver([didIonResolver]); + + const mockResolution = { + didDocument : undefined, + didResolutionMetadata : 'any' as any, + didDocumentMetadata : 'any' as any + }; + + const ionDidResolveSpy = sinon.stub(didIonResolver, 'resolve').resolves(mockResolution); + const cacheGetSpy = sinon.spy(didResolver['cache'], 'get'); + + await expect(didResolver.resolve(did)).to.eventually.be.rejected.and.has.property('code', DwnErrorCode.DidResolutionFailed); + + sinon.assert.calledOnce(cacheGetSpy); + sinon.assert.calledOnce(ionDidResolveSpy); + + }); + }); diff --git a/tests/did/did.spec.ts b/tests/did/did.spec.ts index 81cb9047f..53a94dc0c 100644 --- a/tests/did/did.spec.ts +++ b/tests/did/did.spec.ts @@ -3,6 +3,8 @@ import chai, { expect } from 'chai'; import { Did } from '../../src/did/did.js'; +import { DwnError } from '../../src/core/dwn-error.js'; + // extends chai to test promises chai.use(chaiAsPromised); @@ -15,6 +17,6 @@ describe('Did.validate', () => { it('should fail validation for valid DIDs', () => { expect(() => Did.validate(null)).to.throw(Error); - expect(() => Did.validate('did:123456789abcdefghijk')).to.throw(Error); + expect(() => Did.validate('did:123456789abcdefghijk')).to.throw(DwnError); }); });