diff --git a/.gitignore b/.gitignore index df21dd82c..552d76a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ +# Ignore underlying DWN stores and indexes +**/DATASTORE/ +**/EVENTLOG/ +**/INDEX/ +**/MESSAGESTORE/ + # bundle metadata bundle-metadata.json -# Created by https://www.toptal.com/developers/gitignore/api/node -# Edit at https://www.toptal.com/developers/gitignore?templates=node - ### Node ### # Logs logs @@ -143,5 +146,3 @@ dist # SvelteKit build / generate output .svelte-kit - -# End of https://www.toptal.com/developers/gitignore/api/node diff --git a/karma.conf.cjs b/karma.conf.cjs index be5a485fa..f49d24fd4 100644 --- a/karma.conf.cjs +++ b/karma.conf.cjs @@ -5,7 +5,10 @@ const playwright = require('playwright'); const esbuildBrowserConfig = require('./build/esbuild-browser-config.cjs'); -// use playwright firefox exec path as run target for webkit tests +// use playwright chrome exec path as run target for chromium tests +process.env.CHROME_BIN = playwright.chromium.executablePath(); + +// use playwright webkit exec path as run target for safari tests process.env.WEBKIT_HEADLESS_BIN = playwright.webkit.executablePath(); // use playwright firefox exec path as run target for firefox tests @@ -19,7 +22,7 @@ module.exports = function (config) { 'karma-webkit-launcher', 'karma-esbuild', 'karma-mocha', - 'karma-mocha-reporter' + 'karma-mocha-reporter', ], // frameworks to use @@ -35,7 +38,7 @@ module.exports = function (config) { // preprocess matching files before serving them to the browser // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor preprocessors: { - 'tests/**/*.spec.js': ['esbuild'] + 'tests/**/*.spec.js': ['esbuild'], }, esbuild: esbuildBrowserConfig, @@ -66,6 +69,6 @@ module.exports = function (config) { // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits - singleRun: true + singleRun: true, }); }; diff --git a/package-lock.json b/package-lock.json index 7f3b59181..3404e8368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tbd54566975/web5", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@tbd54566975/web5", - "version": "0.6.0", + "version": "0.6.1", "license": "Apache-2.0", "dependencies": { "@decentralized-identity/ion-tools": "1.0.7", diff --git a/package.json b/package.json index 8a230fa1c..851de006c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tbd54566975/web5", - "version": "0.6.0", + "version": "0.6.1", "description": "SDK for accessing the features and capabilities of Web5", "type": "module", "main": "./dist/cjs/index.cjs", @@ -28,7 +28,7 @@ "lint": "eslint . --ext .js --max-warnings 0", "lint:fix": "eslint . --ext .js --fix", "publish:unstable": "./build/publish-unstable.sh", - "test:node": "mocha \"tests/**/*.spec.js\"", + "test:node": "mocha \"tests/**/*.spec.js\" --exit", "test:browser": "karma start karma.conf.cjs" }, "keywords": [], diff --git a/src/dwn/interfaces/records.js b/src/dwn/interfaces/records.js index 95c70ea2f..5b707c7ae 100644 --- a/src/dwn/interfaces/records.js +++ b/src/dwn/interfaces/records.js @@ -76,7 +76,7 @@ export class Records extends Interface { let dataBytes, dataFormat; if (request?.data) { // If `data` is specified, convert string/object data to bytes before further processing. - ({ dataBytes, dataFormat } = dataToBytes(request.data, request.message.dataFormat)); + ({ dataBytes, dataFormat } = dataToBytes(request.data, request.message?.dataFormat)); } else { // If not, `dataFormat` must be specified in the request message. dataFormat = request.message.dataFormat; diff --git a/src/dwn/models/record.js b/src/dwn/models/record.js index b75ebb4c1..c3d660f68 100644 --- a/src/dwn/models/record.js +++ b/src/dwn/models/record.js @@ -98,7 +98,7 @@ export class Record { }, async text() { if (self.#encodedData) return Encoder.bytesToString(self.#encodedData); - if (self.#readableStream) return self.#readableStream.then(DataStream.toBytes).then(Encoder.bytesToString); + if (self.#readableStream) return this.stream().then(DataStream.toBytes).then(Encoder.bytesToString); return null; }, async stream() { diff --git a/tests/did/manager.spec.js b/tests/did/manager.spec.js new file mode 100644 index 000000000..f8b3919a5 --- /dev/null +++ b/tests/did/manager.spec.js @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { Web5Did } from '../../src/did/web5-did.js'; + +describe('DidManager', async () => { + let web5did; + + beforeEach(function () { + web5did = new Web5Did(); + }); + + before(function () { + this.clock = sinon.useFakeTimers(); + }); + + after(function () { + this.clock.restore(); + }); + + it('should never expire managed DIDs', async function () { + let resolved; + const did = 'did:ion:abcd1234'; + const didData = { + connected: true, + endpoint: 'http://localhost:55500', + }; + + await web5did.manager.set(did, didData); + + resolved = await web5did.resolve(did); + expect(resolved).to.not.be.undefined; + expect(resolved).to.equal(didData); + + this.clock.tick(2147483647); // Time travel 23.85 days + + resolved = await web5did.resolve(did); + expect(resolved).to.not.be.undefined; + expect(resolved).to.equal(didData); + }); + + it('should return object with keys undefined if key data not provided', async () => { + const did = 'did:ion:abcd1234'; + const didData = { + connected: true, + endpoint: 'http://localhost:55500', + }; + + await web5did.manager.set(did, didData); + + const resolved = await web5did.resolve(did); + expect(resolved.keys).to.be.undefined; + }); +}); diff --git a/tests/did/methods/ion.spec.js b/tests/did/methods/ion.spec.js index 986167492..85a40d7cf 100644 --- a/tests/did/methods/ion.spec.js +++ b/tests/did/methods/ion.spec.js @@ -2,9 +2,9 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { Web5Did } from '../../../src/did/web5-did.js'; -import * as didDocuments from '../../data/did-documents.js'; +import * as didDocuments from '../../fixtures/did-documents.js'; -describe('Web5Did', async () => { +describe('did:ion method', async () => { let web5did; beforeEach(function () { @@ -19,7 +19,7 @@ describe('Web5Did', async () => { this.clock.restore(); }); - describe('create', async () => { + describe('create()', async () => { it('should return one key when creating a did:ion DID', async () => { const did = await web5did.create('ion'); expect(did.keys).to.have.lengthOf(1); @@ -43,7 +43,7 @@ describe('Web5Did', async () => { }); }); - describe('getDidDocument', async () => { + describe('getDidDocument()', async () => { it('should return a didDocument for a valid did:ion DID', async () => { sinon.stub(web5did, 'resolve').resolves(didDocuments.ion.oneVerificationMethodJwk); @@ -62,7 +62,7 @@ describe('Web5Did', async () => { }); }); - describe('getServices', async () => { + describe('getServices()', async () => { it('should return array of services when defined in DID document', async () => { sinon.stub(web5did, 'resolve').resolves(didDocuments.ion.oneService); @@ -81,7 +81,7 @@ describe('Web5Did', async () => { }); }); - describe('resolve', async () => { + describe('resolve()', async () => { it('should not call ion-tools resolve() when managed DID is cached', async () => { // If managed DID isn't cached, the fetch() call to resolve over the network // will take far more than 10ms timeout, causing the test to fail. @@ -114,4 +114,4 @@ describe('Web5Did', async () => { expect(resolved.didResolutionMetadata.error).to.equal(`unable to resolve ${did}, got http status 404`); }); }); -}); \ No newline at end of file +}); diff --git a/tests/did/methods/key.spec.js b/tests/did/methods/key.spec.js index 2132c828a..82aaf56f8 100644 --- a/tests/did/methods/key.spec.js +++ b/tests/did/methods/key.spec.js @@ -2,16 +2,16 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { Web5Did } from '../../../src/did/web5-did.js'; -import * as didDocuments from '../../data/did-documents.js'; +import * as didDocuments from '../../fixtures/did-documents.js'; -describe('Web5Did', async () => { +describe('did:key method', async () => { let web5did; beforeEach(function () { web5did = new Web5Did(); }); - describe('create', async () => { + describe('create()', async () => { it('should return two keys when creating a did:key DID', async () => { const did = await web5did.create('key'); expect(did.keys).to.have.lengthOf(2); @@ -44,7 +44,7 @@ describe('Web5Did', async () => { }); }); - describe('getDidDocument', async () => { + describe('getDidDocument()', async () => { it('should return a didDocument for a valid did:key DID', async () => { sinon.stub(web5did, 'resolve').resolves(didDocuments.key.oneVerificationMethodJwk); @@ -63,7 +63,7 @@ describe('Web5Did', async () => { }); }); - describe('resolve', async () => { + describe('resolve()', async () => { it('should return a didResolutionResult for a valid DID', async () => { const did = 'did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9'; @@ -82,4 +82,4 @@ describe('Web5Did', async () => { expect(resolved.didResolutionMetadata.error).to.equal('invalidDid'); }); }); -}); \ No newline at end of file +}); diff --git a/tests/did/utils.spec.js b/tests/did/utils.spec.js index 2a71b5c8b..1d2a7988f 100644 --- a/tests/did/utils.spec.js +++ b/tests/did/utils.spec.js @@ -2,259 +2,256 @@ import chaiAsPromised from 'chai-as-promised'; import chai, { expect } from 'chai'; import * as DidUtils from '../../src/did/utils.js'; -import * as didDocuments from '../data/did-documents.js'; +import * as didDocuments from '../fixtures/did-documents.js'; chai.use(chaiAsPromised); -describe('Web5Did', async () => { +describe('DID Utils', () => { + + describe('decodeMultibaseBase58()', () => { + it('should pass the Multibase Data Format example', async () => { + // Example from https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03#section-2.1 + const testVectorInput = 'z2NEpo7TZRRrLZSi2U'; + const testVectorResult = 'Hello World!'; + const NO_HEADER = new Uint8Array([]); + const resultBytes = await DidUtils.decodeMultibaseBase58(testVectorInput, NO_HEADER); + const resultString = new TextDecoder().decode(resultBytes); + expect(resultString).to.equal(testVectorResult); + }); - describe('DID Utils Tests', () => { + it('should pass the Multibase Data Format test vector', async () => { + // Test Vector from https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03#appendix-B.3 + const testVectorInput = 'zYAjKoNbau5KiqmHPmSxYCvn66dA1vLmwbt'; + const testVectorResult = 'Multibase is awesome! \\o/'; + const NO_HEADER = new Uint8Array([]); + const resultBytes = await DidUtils.decodeMultibaseBase58(testVectorInput, NO_HEADER); + const resultString = new TextDecoder().decode(resultBytes); + expect(resultString).to.equal(testVectorResult); + }); + }); - describe('decodeMultibaseBase58', () => { - it('should pass the Multibase Data Format example', async () => { - // Example from https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03#section-2.1 - const testVectorInput = 'z2NEpo7TZRRrLZSi2U'; - const testVectorResult = 'Hello World!'; - const NO_HEADER = new Uint8Array([]); - const resultBytes = await DidUtils.decodeMultibaseBase58(testVectorInput, NO_HEADER); - const resultString = new TextDecoder().decode(resultBytes); - expect(resultString).to.equal(testVectorResult); - }); + describe('findVerificationMethods()', () => { - it('should pass the Multibase Data Format test vector', async () => { - // Test Vector from https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03#appendix-B.3 - const testVectorInput = 'zYAjKoNbau5KiqmHPmSxYCvn66dA1vLmwbt'; - const testVectorResult = 'Multibase is awesome! \\o/'; - const NO_HEADER = new Uint8Array([]); - const resultBytes = await DidUtils.decodeMultibaseBase58(testVectorInput, NO_HEADER); - const resultString = new TextDecoder().decode(resultBytes); - expect(resultString).to.equal(testVectorResult); - }); + it('should throw error if didDocument is missing', () => { + expect(() => DidUtils.findVerificationMethods({ methodId: 'did:key:abcd1234' })).to.throw('didDocument is a required parameter'); }); - describe('findVerificationMethods', () => { + it('should not throw an error if purpose and methodId are both missing', () => { + expect(() => DidUtils.findVerificationMethods({ didDocument: {} })).to.not.throw(); + }); - it('should throw error if didDocument is missing', () => { - expect(() => DidUtils.findVerificationMethods({ methodId: 'did:key:abcd1234' })).to.throw('didDocument is a required parameter'); - }); + it('should throw error if purpose and methodId are both specified', () => { + expect(() => DidUtils.findVerificationMethods({ didDocument: {}, methodId: ' ', purpose: ' ' })).to.throw('Specify methodId or purpose but not both'); + }); - it('should not throw an error if purpose and methodId are both missing', () => { - expect(() => DidUtils.findVerificationMethods({ didDocument: {} })).to.not.throw(); - }); + describe('by methodId', () => { + it('should return single verification method when defined in single method DID document', () => { + const didDocument = didDocuments.key.oneVerificationMethodJwk.didDocument; + const methodId = didDocuments.key.oneVerificationMethodJwk.didDocument.verificationMethod[0].id; - it('should throw error if purpose and methodId are both specified', () => { - expect(() => DidUtils.findVerificationMethods({ didDocument: {}, methodId: ' ', purpose: ' ' })).to.throw('Specify methodId or purpose but not both'); + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId }); + + expect(verificationMethods).to.have.lengthOf(1); + expect(verificationMethods[0]).to.be.an('object'); + expect(verificationMethods[0]).to.have.property('id', methodId); }); - describe('by methodId', () => { - it('should return single verification method when defined in single method DID document', () => { - const didDocument = didDocuments.key.oneVerificationMethodJwk.didDocument; - const methodId = didDocuments.key.oneVerificationMethodJwk.didDocument.verificationMethod[0].id; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId }); - - expect(verificationMethods).to.have.lengthOf(1); - expect(verificationMethods[0]).to.be.an('object'); - expect(verificationMethods[0]).to.have.property('id', methodId); - }); - - it('should return single verification method when defined in multi method DID document', () => { - const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; - const methodId = didDocuments.key.manyVerificationMethodsJwk.didDocument.verificationMethod[1].id; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId }); - - expect(verificationMethods).to.have.lengthOf(1); - expect(verificationMethods[0]).to.be.an('object'); - expect(verificationMethods[0]).to.have.property('id', methodId); - }); - - it('should return single verification method when embedded in purpose', () => { - const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; - const methodId = didDocuments.key.manyVerificationMethodsJwk.didDocument['keyAgreement'][1].id; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId }); - - expect(verificationMethods).to.have.lengthOf(1); - expect(verificationMethods[0]).to.be.an('object'); - expect(verificationMethods[0]).to.have.property('id', methodId); - }); - - it('should return null if verification method ID not found', () => { - const didDocument = didDocuments.key.oneVerificationMethodJwk.didDocument; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId: 'did:key:abcd1234#def980' }); - - expect(verificationMethods).to.be.null; - }); - - it('should return one verification method when no method ID is specified in single method DID document', () => { - const didDocument = didDocuments.key.oneVerificationMethodJwk.didDocument; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument }); - - expect(verificationMethods).to.have.lengthOf(1); - expect(verificationMethods[0]).to.be.an('object'); - }); - - it('should return all verification methods when no method ID is specified in single method DID document', () => { - const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument }); - - expect(verificationMethods).to.have.lengthOf(8); - expect(verificationMethods[0]).to.have.property('id'); - expect(verificationMethods[1]).to.have.property('id'); - expect(verificationMethods[2]).to.have.property('id'); - expect(verificationMethods[3]).to.have.property('id'); - expect(verificationMethods[4]).to.have.property('id'); - expect(verificationMethods[5]).to.have.property('id'); - expect(verificationMethods[6]).to.have.property('id'); - expect(verificationMethods[7]).to.have.property('id'); - }); + it('should return single verification method when defined in multi method DID document', () => { + const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; + const methodId = didDocuments.key.manyVerificationMethodsJwk.didDocument.verificationMethod[1].id; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId }); + + expect(verificationMethods).to.have.lengthOf(1); + expect(verificationMethods[0]).to.be.an('object'); + expect(verificationMethods[0]).to.have.property('id', methodId); }); - describe('by purpose', () => { - it('should return an array of verification methods if multiple referenced methods are defined', () => { - const didDocument = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument; - const purpose = 'authentication'; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); - - expect(verificationMethods).to.be.an('Array'); - expect(verificationMethods).to.have.lengthOf(2); - const keyId1 = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument.verificationMethod[0].id; - expect(verificationMethods[0]).to.have.property('id', keyId1); - const keyId2 = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument.verificationMethod[1].id; - expect(verificationMethods[1]).to.have.property('id', keyId2); - }); - - it('should return an array of verification methods if multiple embedded methods are defined', () => { - const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; - const purpose = 'keyAgreement'; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); - - expect(verificationMethods).to.be.an('Array'); - expect(verificationMethods).to.have.lengthOf(2); - const keyId1 = didDocuments.key.manyVerificationMethodsJwk.didDocument['keyAgreement'][0].id; - expect(verificationMethods[0]).to.have.property('id', keyId1); - const keyId2 = didDocuments.key.manyVerificationMethodsJwk.didDocument['keyAgreement'][1].id; - expect(verificationMethods[1]).to.have.property('id', keyId2); - }); - - it('should return an array of verification methods if referenced and embedded methods are defined', () => { - const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; - const purpose = 'authentication'; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); - - expect(verificationMethods).to.be.an('Array'); - expect(verificationMethods).to.have.lengthOf(3); - const keyId1 = didDocuments.key.manyVerificationMethodsJwk.didDocument.verificationMethod[0].id; - expect(verificationMethods[0]).to.have.property('id', keyId1); - const keyId2 = didDocuments.key.manyVerificationMethodsJwk.didDocument['authentication'][1].id; - expect(verificationMethods[1]).to.have.property('id', keyId2); - const keyId3 = didDocuments.key.manyVerificationMethodsJwk.didDocument['authentication'][2].id; - expect(verificationMethods[2]).to.have.property('id', keyId3); - }); - - it('should return null if purpose not found', () => { - const didDocument = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument; - const purpose = 'keyAgreement'; - - const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); - - expect(verificationMethods).to.be.null; - }); + it('should return single verification method when embedded in purpose', () => { + const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; + const methodId = didDocuments.key.manyVerificationMethodsJwk.didDocument['keyAgreement'][1].id; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId }); + + expect(verificationMethods).to.have.lengthOf(1); + expect(verificationMethods[0]).to.be.an('object'); + expect(verificationMethods[0]).to.have.property('id', methodId); }); - }); - describe('verificationMethodToPublicKeyBytes', () => { - it('should produce 32-byte key from JsonWebKey2020 Ed25519', async () => { - const testVectorInput = { - id: 'did:key:z6MkvWGGZCXi3F7rY6ZLvaq5dGYMbvpLg95XFX3srkqdFVXE#z6MkvWGGZCXi3F7rY6ZLvaq5dGYMbvpLg95XFX3srkqdFVXE', - type: 'JsonWebKey2020', - controller: 'did:key:z6MkvWGGZCXi3F7rY6ZLvaq5dGYMbvpLg95XFX3srkqdFVXE', - publicKeyJwk: { - kty: 'OKP', - crv: 'Ed25519', - x: '7n_8aiGRMBlsJHQd4t35n307na7TZNElysEBREpvRnk', - }, - }; - const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); - expect(resultBytes).to.have.length(32); + it('should return null if verification method ID not found', () => { + const didDocument = didDocuments.key.oneVerificationMethodJwk.didDocument; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, methodId: 'did:key:abcd1234#def980' }); + + expect(verificationMethods).to.be.null; }); - it('should produce 65-byte key from JsonWebKey2020 secp256k1', async () => { - const testVectorInput = { - id: 'did:key:zQ3shvj64zaZJnx4dGrW2Hys6xC9UFMEjz4nkzEJmH1Vh9fGf#zQ3shvj64zaZJnx4dGrW2Hys6xC9UFMEjz4nkzEJmH1Vh9fGf', - type: 'JsonWebKey2020', - controller: 'did:key:zQ3shvj64zaZJnx4dGrW2Hys6xC9UFMEjz4nkzEJmH1Vh9fGf', - publicKeyJwk: { - kty: 'EC', - crv: 'secp256k1', - x: '7wGy8EIWkdDOnDHWT7e8R8tkKYbYCurodKQNtLAeaiQ', - y: '7CnORxKJqon8qscaGY_nWROEn2B4oSBxtEryIUn3buc', - }, - }; - const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); - expect(resultBytes).to.have.length(65); + it('should return one verification method when no method ID is specified in single method DID document', () => { + const didDocument = didDocuments.key.oneVerificationMethodJwk.didDocument; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument }); + + expect(verificationMethods).to.have.lengthOf(1); + expect(verificationMethods[0]).to.be.an('object'); }); - it('should produce 32-byte key from Ed25519VerificationKey2018', async () => { - const testVectorInput = { - id: 'did:key:z6MkowGxLDf5oVh8FUXHEo2GG2RsyvS4jNHFWoDeM96aU7pR#z6MkowGxLDf5oVh8FUXHEo2GG2RsyvS4jNHFWoDeM96aU7pR', - type: 'Ed25519VerificationKey2018', - controller: 'did:key:z6MkowGxLDf5oVh8FUXHEo2GG2RsyvS4jNHFWoDeM96aU7pR', - publicKeyBase58: 'AV1ujyQeTxCf8ygaZE4RQvstAMADKV2tpnJiWs8ZYu33', - }; - const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); - expect(resultBytes).to.have.length(32); + it('should return all verification methods when no method ID is specified in single method DID document', () => { + const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument }); + + expect(verificationMethods).to.have.lengthOf(8); + expect(verificationMethods[0]).to.have.property('id'); + expect(verificationMethods[1]).to.have.property('id'); + expect(verificationMethods[2]).to.have.property('id'); + expect(verificationMethods[3]).to.have.property('id'); + expect(verificationMethods[4]).to.have.property('id'); + expect(verificationMethods[5]).to.have.property('id'); + expect(verificationMethods[6]).to.have.property('id'); + expect(verificationMethods[7]).to.have.property('id'); }); + }); - it('should produce 32-byte key from Ed25519VerificationKey2020', async () => { - const testVectorInput = { - id: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe#z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', - controller: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', - type: 'Ed25519VerificationKey2020', - publicKeyMultibase: 'z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', - }; - const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); - expect(resultBytes).to.have.length(32); + describe('by purpose', () => { + it('should return an array of verification methods if multiple referenced methods are defined', () => { + const didDocument = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument; + const purpose = 'authentication'; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); + + expect(verificationMethods).to.be.an('Array'); + expect(verificationMethods).to.have.lengthOf(2); + const keyId1 = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument.verificationMethod[0].id; + expect(verificationMethods[0]).to.have.property('id', keyId1); + const keyId2 = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument.verificationMethod[1].id; + expect(verificationMethods[1]).to.have.property('id', keyId2); }); - it('should produce 32-byte key from X25519KeyAgreementKey2019', async () => { - const testVectorInput = { - id: 'did:key:z6LSi7dU7FZfD8P71j5PJrBugYuhvWgPTP8kt9SvXM2moGCy#z6LSi7dU7FZfD8P71j5PJrBugYuhvWgPTP8kt9SvXM2moGCy', - type: 'X25519KeyAgreementKey2019', - controller: 'did:key:z6LSi7dU7FZfD8P71j5PJrBugYuhvWgPTP8kt9SvXM2moGCy', - publicKeyBase58: '7STJawko7ffMvLhcnCfxMxhE5N9Gkmxc1AjF2tPF5tSD', - }; - const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); - expect(resultBytes).to.have.length(32); + it('should return an array of verification methods if multiple embedded methods are defined', () => { + const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; + const purpose = 'keyAgreement'; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); + + expect(verificationMethods).to.be.an('Array'); + expect(verificationMethods).to.have.lengthOf(2); + const keyId1 = didDocuments.key.manyVerificationMethodsJwk.didDocument['keyAgreement'][0].id; + expect(verificationMethods[0]).to.have.property('id', keyId1); + const keyId2 = didDocuments.key.manyVerificationMethodsJwk.didDocument['keyAgreement'][1].id; + expect(verificationMethods[1]).to.have.property('id', keyId2); }); - it('should produce 32-byte key from X25519KeyAgreementKey2020', async () => { - const testVectorInput = { - id: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe#z6LSmWBeTmbc52MeApriadmSupqXqAjvqEQ64TY2bVjmbiw3', - controller: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', - type: 'X25519KeyAgreementKey2020', - publicKeyMultibase: 'z6LSmWBeTmbc52MeApriadmSupqXqAjvqEQ64TY2bVjmbiw3', - }; - const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); - expect(resultBytes).to.have.length(32); + it('should return an array of verification methods if referenced and embedded methods are defined', () => { + const didDocument = didDocuments.key.manyVerificationMethodsJwk.didDocument; + const purpose = 'authentication'; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); + + expect(verificationMethods).to.be.an('Array'); + expect(verificationMethods).to.have.lengthOf(3); + const keyId1 = didDocuments.key.manyVerificationMethodsJwk.didDocument.verificationMethod[0].id; + expect(verificationMethods[0]).to.have.property('id', keyId1); + const keyId2 = didDocuments.key.manyVerificationMethodsJwk.didDocument['authentication'][1].id; + expect(verificationMethods[1]).to.have.property('id', keyId2); + const keyId3 = didDocuments.key.manyVerificationMethodsJwk.didDocument['authentication'][2].id; + expect(verificationMethods[2]).to.have.property('id', keyId3); }); - it('should throw an error for unsupported verification method type', async () => { - const testVectorInput = { - id: 'did:key:z5TcEqLQRZagxohf4kbuku7tX1UfNKR3FawBUKuWMu12tKzEeAzjHcjHk2ewc7esb6f1izCBVrzTF16ec2fC95fzbMMtcPpvg4BY3uyfp6f89JdBZCLEtzfJtTM7p2MQF7hgtiUnuKFVcJKpL32cUta7Vr8vZ3uPYNoXgUCdbFEZgMre3AKSxHTjSPDqBSHLQdYeFzLaL#z3tEGLqaQnrqx1KbDNSEdyV7C8Mz5bkPMF2TfvL7tcSrutXCJL9ehWjahbvfNdbkFBPdA4', - type: 'Bls12381G1Key2020', - controller: 'did:key:z5TcEqLQRZagxohf4kbuku7tX1UfNKR3FawBUKuWMu12tKzEeAzjHcjHk2ewc7esb6f1izCBVrzTF16ec2fC95fzbMMtcPpvg4BY3uyfp6f89JdBZCLEtzfJtTM7p2MQF7hgtiUnuKFVcJKpL32cUta7Vr8vZ3uPYNoXgUCdbFEZgMre3AKSxHTjSPDqBSHLQdYeFzLaL', - publicKeyBase58: '7SV3YJ2vwE1jHfqvJTgjBoGH44gepRzahyafHJF1kW23sc1FeUtYD4EU3pSBo8mVp8', - }; - await expect(DidUtils.verificationMethodToPublicKeyBytes(testVectorInput)).to.be.rejectedWith('Unsupported verification method type: Bls12381G1Key2020'); + it('should return null if purpose not found', () => { + const didDocument = didDocuments.key.twoAuthenticationReferencedKeysJwk.didDocument; + const purpose = 'keyAgreement'; + + const verificationMethods = DidUtils.findVerificationMethods({ didDocument, purpose }); + + expect(verificationMethods).to.be.null; }); }); }); + + describe('verificationMethodToPublicKeyBytes()', () => { + it('should produce 32-byte key from JsonWebKey2020 Ed25519', async () => { + const testVectorInput = { + id: 'did:key:z6MkvWGGZCXi3F7rY6ZLvaq5dGYMbvpLg95XFX3srkqdFVXE#z6MkvWGGZCXi3F7rY6ZLvaq5dGYMbvpLg95XFX3srkqdFVXE', + type: 'JsonWebKey2020', + controller: 'did:key:z6MkvWGGZCXi3F7rY6ZLvaq5dGYMbvpLg95XFX3srkqdFVXE', + publicKeyJwk: { + kty: 'OKP', + crv: 'Ed25519', + x: '7n_8aiGRMBlsJHQd4t35n307na7TZNElysEBREpvRnk', + }, + }; + const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); + expect(resultBytes).to.have.length(32); + }); + + it('should produce 65-byte key from JsonWebKey2020 secp256k1', async () => { + const testVectorInput = { + id: 'did:key:zQ3shvj64zaZJnx4dGrW2Hys6xC9UFMEjz4nkzEJmH1Vh9fGf#zQ3shvj64zaZJnx4dGrW2Hys6xC9UFMEjz4nkzEJmH1Vh9fGf', + type: 'JsonWebKey2020', + controller: 'did:key:zQ3shvj64zaZJnx4dGrW2Hys6xC9UFMEjz4nkzEJmH1Vh9fGf', + publicKeyJwk: { + kty: 'EC', + crv: 'secp256k1', + x: '7wGy8EIWkdDOnDHWT7e8R8tkKYbYCurodKQNtLAeaiQ', + y: '7CnORxKJqon8qscaGY_nWROEn2B4oSBxtEryIUn3buc', + }, + }; + const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); + expect(resultBytes).to.have.length(65); + }); + + it('should produce 32-byte key from Ed25519VerificationKey2018', async () => { + const testVectorInput = { + id: 'did:key:z6MkowGxLDf5oVh8FUXHEo2GG2RsyvS4jNHFWoDeM96aU7pR#z6MkowGxLDf5oVh8FUXHEo2GG2RsyvS4jNHFWoDeM96aU7pR', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkowGxLDf5oVh8FUXHEo2GG2RsyvS4jNHFWoDeM96aU7pR', + publicKeyBase58: 'AV1ujyQeTxCf8ygaZE4RQvstAMADKV2tpnJiWs8ZYu33', + }; + const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); + expect(resultBytes).to.have.length(32); + }); + + it('should produce 32-byte key from Ed25519VerificationKey2020', async () => { + const testVectorInput = { + id: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe#z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', + controller: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', + type: 'Ed25519VerificationKey2020', + publicKeyMultibase: 'z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', + }; + const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); + expect(resultBytes).to.have.length(32); + }); + + it('should produce 32-byte key from X25519KeyAgreementKey2019', async () => { + const testVectorInput = { + id: 'did:key:z6LSi7dU7FZfD8P71j5PJrBugYuhvWgPTP8kt9SvXM2moGCy#z6LSi7dU7FZfD8P71j5PJrBugYuhvWgPTP8kt9SvXM2moGCy', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6LSi7dU7FZfD8P71j5PJrBugYuhvWgPTP8kt9SvXM2moGCy', + publicKeyBase58: '7STJawko7ffMvLhcnCfxMxhE5N9Gkmxc1AjF2tPF5tSD', + }; + const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); + expect(resultBytes).to.have.length(32); + }); + + it('should produce 32-byte key from X25519KeyAgreementKey2020', async () => { + const testVectorInput = { + id: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe#z6LSmWBeTmbc52MeApriadmSupqXqAjvqEQ64TY2bVjmbiw3', + controller: 'did:key:z6MkuBzvPSJafL9JZEj3Cp5CuG1rPyXsZbynHqtYjecxR2pe', + type: 'X25519KeyAgreementKey2020', + publicKeyMultibase: 'z6LSmWBeTmbc52MeApriadmSupqXqAjvqEQ64TY2bVjmbiw3', + }; + const resultBytes = await DidUtils.verificationMethodToPublicKeyBytes(testVectorInput); + expect(resultBytes).to.have.length(32); + }); + + it('should throw an error for unsupported verification method type', async () => { + const testVectorInput = { + id: 'did:key:z5TcEqLQRZagxohf4kbuku7tX1UfNKR3FawBUKuWMu12tKzEeAzjHcjHk2ewc7esb6f1izCBVrzTF16ec2fC95fzbMMtcPpvg4BY3uyfp6f89JdBZCLEtzfJtTM7p2MQF7hgtiUnuKFVcJKpL32cUta7Vr8vZ3uPYNoXgUCdbFEZgMre3AKSxHTjSPDqBSHLQdYeFzLaL#z3tEGLqaQnrqx1KbDNSEdyV7C8Mz5bkPMF2TfvL7tcSrutXCJL9ehWjahbvfNdbkFBPdA4', + type: 'Bls12381G1Key2020', + controller: 'did:key:z5TcEqLQRZagxohf4kbuku7tX1UfNKR3FawBUKuWMu12tKzEeAzjHcjHk2ewc7esb6f1izCBVrzTF16ec2fC95fzbMMtcPpvg4BY3uyfp6f89JdBZCLEtzfJtTM7p2MQF7hgtiUnuKFVcJKpL32cUta7Vr8vZ3uPYNoXgUCdbFEZgMre3AKSxHTjSPDqBSHLQdYeFzLaL', + publicKeyBase58: '7SV3YJ2vwE1jHfqvJTgjBoGH44gepRzahyafHJF1kW23sc1FeUtYD4EU3pSBo8mVp8', + }; + await expect(DidUtils.verificationMethodToPublicKeyBytes(testVectorInput)).to.be.rejectedWith('Unsupported verification method type: Bls12381G1Key2020'); + }); + }); }); diff --git a/tests/did/web5-did.spec.js b/tests/did/web5-did.spec.js index bd9652459..ef99aa875 100644 --- a/tests/did/web5-did.spec.js +++ b/tests/did/web5-did.spec.js @@ -1,11 +1,11 @@ import { expect } from 'chai'; +import { Encoder } from '@tbd54566975/dwn-sdk-js'; import sinon from 'sinon'; -import { Encoder } from '@tbd54566975/dwn-sdk-js'; import { base64UrlToString } from '../../src/utils.js'; import { Web5 } from '../../src/web5.js'; import { Web5Did } from '../../src/did/web5-did.js'; -import * as didDocuments from '../data/did-documents.js'; +import * as didDocuments from '../fixtures/did-documents.js'; describe('Web5Did', async () => { let web5did; @@ -22,7 +22,7 @@ describe('Web5Did', async () => { this.clock.restore(); }); - describe('decrypt', () => { + describe('decrypt()', () => { let web5; beforeEach(function () { @@ -51,7 +51,7 @@ describe('Web5Did', async () => { }); }); - describe('encrypt', () => { + describe('encrypt()', () => { let web5; beforeEach(function () { @@ -118,7 +118,7 @@ describe('Web5Did', async () => { }); }); - describe('getKeys', async () => { + describe('getKeys()', async () => { it('should return one key when one verification method is defined in DID document', async () => { sinon.stub(web5did, 'resolve').resolves(didDocuments.ion.oneVerificationMethodJwk); @@ -145,41 +145,4 @@ describe('Web5Did', async () => { expect(didKeys).to.be.null; }); }); - - - describe('manager', async () => { - it('should never expire managed DIDs', async function () { - let resolved; - const did = 'did:ion:abcd1234'; - const didData = { - connected: true, - endpoint: 'http://localhost:55500', - }; - - await web5did.manager.set(did, didData); - - resolved = await web5did.resolve(did); - expect(resolved).to.not.be.undefined; - expect(resolved).to.equal(didData); - - this.clock.tick(2147483647); // Time travel 23.85 days - - resolved = await web5did.resolve(did); - expect(resolved).to.not.be.undefined; - expect(resolved).to.equal(didData); - }); - - it('should return object with keys undefined if key data not provided', async () => { - const did = 'did:ion:abcd1234'; - const didData = { - connected: true, - endpoint: 'http://localhost:55500', - }; - - await web5did.manager.set(did, didData); - - const resolved = await web5did.resolve(did); - expect(resolved.keys).to.be.undefined; - }); - }); -}); \ No newline at end of file +}); diff --git a/tests/dwn/interfaces/records.spec.js b/tests/dwn/interfaces/records.spec.js new file mode 100644 index 000000000..c32584a8f --- /dev/null +++ b/tests/dwn/interfaces/records.spec.js @@ -0,0 +1,195 @@ +import chaiAsPromised from 'chai-as-promised'; +import chai, { expect } from 'chai'; +import { DataStream } from '@tbd54566975/dwn-sdk-js'; + +import { Web5 } from '../../../src/web5.js'; + +import { TestDataGenerator } from '../../test-utils/test-data-generator.js'; +import { TestDwn } from '../../test-utils/test-dwn.js'; + +chai.use(chaiAsPromised); + +describe('Records', async () => { + let testDwn, web5; + let alice; + + before(async () => { + testDwn = await TestDwn.create(); + web5 = new Web5({ dwn: { node: testDwn.node } }); + + alice = await web5.did.create('ion'); + + await web5.did.manager.set(alice.id, { + connected: true, + endpoint: 'app://dwn', + keys: { + ['#dwn']: { + keyPair: alice.keys.find(key => key.id === 'dwn').keyPair, + }, + }, + }); + }); + + beforeEach(async () => { + // Clean up before each test rather than after so that a test does not depend on other tests to do the clean up. + await testDwn.clear(); + }); + + after(async () => { + // Close connections to the underlying stores. + await testDwn.close(); + }); + + describe('read()', () => { + let readResponse, jsonWriteResponse; + + beforeEach(async () => { + // write a record that can be read from for each test + jsonWriteResponse = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: { message: 'Hello, world!' }, + message: { + dataFormat: 'application/json', + }, + }); + }); + + describe('unit tests', () => { + it('should return message, record, and status', async () => { + readResponse = await web5.dwn.records.read(alice.id, { + author: alice.id, + message: { + recordId: jsonWriteResponse.record.id, + }, + }); + + expect(readResponse).to.have.property('message'); + expect(readResponse).to.have.property('record'); + expect(readResponse).to.have.property('status'); + }); + }); + }); + + describe('write()', () => { + describe('unit tests', () => { + it('should return data, entries, message, record, and status', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: { message: 'Hello, world!' }, + message: { + dataFormat: 'application/json', + }, + }); + expect(response).to.have.property('data'); + expect(response).to.have.property('entries'); + expect(response).to.have.property('message'); + expect(response).to.have.property('record'); + expect(response).to.have.property('status'); + }); + + it('should accept as input string data with no dataFormat', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: 'Hello, world!', + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'text/plain'); + await expect(response.record.data.text()).to.eventually.equal('Hello, world!'); + }); + + it('should accept as input string data with dataFormat specified', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: 'Hello, world!', + message: { + dataFormat: 'text/plain', + }, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'text/plain'); + await expect(response.record.data.text()).to.eventually.equal('Hello, world!'); + }); + + it('should accept as input JSON data with no dataFormat', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: { message: 'Hello, world!' }, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'application/json'); + await expect(response.record.data.json()).to.eventually.deep.equal({ message: 'Hello, world!' }); + }); + + it('should accept as input JSON data with dataFormat specified', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: { message: 'Hello, world!' }, + message: { + dataFormat: 'application/json', + }, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'application/json'); + await expect(response.record.data.json()).to.eventually.deep.equal({ message: 'Hello, world!' }); + }); + + it('should accept as input JSON data with no dataFormat', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: { message: 'Hello, world!' }, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'application/json'); + await expect(response.record.data.json()).to.eventually.deep.equal({ message: 'Hello, world!' }); + }); + + it('should accept as input JSON data with dataFormat specified', async () => { + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: { message: 'Hello, world!' }, + message: { + dataFormat: 'application/json', + }, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'application/json'); + await expect(response.record.data.json()).to.eventually.deep.equal({ message: 'Hello, world!' }); + }); + + it('should accept as input binary data with no dataFormat', async () => { + const dataBytes = TestDataGenerator.randomBytes(32); + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: dataBytes, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'application/octet-stream'); + const responseData = await DataStream.toBytes(await response.record.data.stream()); + expect(responseData).to.deep.equal(dataBytes); + }); + + it('should accept as input binary data with dataFormat specified', async () => { + const dataBytes = TestDataGenerator.randomBytes(32); + const response = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: dataBytes, + message: { + dataFormat: 'image/png', + }, + }); + + expect(response).to.have.nested.property('status.code', 202); + expect(response).to.have.nested.property('record.dataFormat', 'image/png'); + const responseData = await DataStream.toBytes(await response.record.data.stream()); + expect(responseData).to.deep.equal(dataBytes); + }); + }); + }); +}); diff --git a/tests/dwn/models/record.spec.js b/tests/dwn/models/record.spec.js new file mode 100644 index 000000000..147e89555 --- /dev/null +++ b/tests/dwn/models/record.spec.js @@ -0,0 +1,152 @@ +import chaiAsPromised from 'chai-as-promised'; +import chai, { expect } from 'chai'; +import { DataStream, DwnConstant } from '@tbd54566975/dwn-sdk-js'; + +import { Web5 } from '../../../src/web5.js'; +import { Record } from '../../../src/dwn/models/record.js'; +import { dataToBytes } from '../../../src/utils.js'; + +import { createTimeoutPromise } from '../../test-utils/promises.js'; +import { TestDataGenerator } from '../../test-utils/test-data-generator.js'; +import { TestDwn } from '../../test-utils/test-dwn.js'; + +chai.use(chaiAsPromised); + +describe('Record', async () => { + let dataBytes, dataFormat, testDwn, web5; + let alice; + + before(async () => { + testDwn = await TestDwn.create(); + web5 = new Web5({ dwn: { node: testDwn.node } }); + + alice = await web5.did.create('ion'); + + await web5.did.manager.set(alice.id, { + connected: true, + endpoint: 'app://dwn', + keys: { + ['#dwn']: { + keyPair: alice.keys.find(key => key.id === 'dwn').keyPair, + }, + }, + }); + }); + + after(async () => { + // Close connections to the underlying stores. + await testDwn.close(); + }); + + describe('created from RecordsRead response', () => { + let jsonWriteResponse; + + describe('when dataSize <= DwnConstant.maxDataSizeAllowedToBeEncoded', () => { + const dataSize = 10; + + describe('data.json()', async () => { + let dataJson; + + before(async () => { + dataJson = TestDataGenerator.generateJson(dataSize); + ({ dataBytes, dataFormat } = dataToBytes(dataJson)); + jsonWriteResponse = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: dataJson, + message: { + dataFormat: dataFormat, + }, + }); + }); + + it('should return JSON many times when instantiated with encoded data', async () => { + const record = new Record(web5.dwn, { + author: alice.id, + descriptor: jsonWriteResponse.message.descriptor, + encodedData: dataBytes, + recordId: jsonWriteResponse.message.recordId, + target: alice.id, + }); + + expect (record.dataSize) <= DwnConstant.maxDataSizeAllowedToBeEncoded; + + // The first invocation should succeed. + await expect(record.data.json()).to.eventually.deep.equal(dataJson); + // Assert that the second call to record.data.json() is fulfilled before the timeout. + await chai.assert.isFulfilled(Promise.race([record.data.json(), createTimeoutPromise(5)])); + // Assert that the third call to record.data.json() is fulfilled before the timeout. + await chai.assert.isFulfilled(Promise.race([record.data.json(), createTimeoutPromise(5)])); + }); + + it('should return JSON once when instantiated without data', async () => { + const record = new Record(web5.dwn, { + author: alice.id, + descriptor: jsonWriteResponse.message.descriptor, + recordId: jsonWriteResponse.message.recordId, + target: alice.id, + }); + + expect (record.dataSize) <= DwnConstant.maxDataSizeAllowedToBeEncoded; + + // The first invocation should succeed. + await expect(record.data.json()).to.eventually.deep.equal(dataJson); + // Assert that the second call to record.data.json() is rejected due to the timeout. + await chai.assert.isRejected(Promise.race([record.data.json(), createTimeoutPromise(5)])); + }); + }); + }); + + describe('when dataSize > DwnConstant.maxDataSizeAllowedToBeEncoded', () => { + const dataSize = DwnConstant.maxDataSizeAllowedToBeEncoded + 1000; + + describe('data.json()', async () => { + let dataJson; + + before(async () => { + dataJson = TestDataGenerator.generateJson(dataSize); + ({ dataBytes, dataFormat } = dataToBytes(dataJson)); + jsonWriteResponse = await web5.dwn.records.write(alice.id, { + author: alice.id, + data: dataJson, + message: { + dataFormat: dataFormat, + }, + }); + }); + + it('should return JSON once when instantiated with a data stream', async () => { + const record = new Record(web5.dwn, { + author: alice.id, + descriptor: jsonWriteResponse.message.descriptor, + data: DataStream.fromBytes(dataBytes), + recordId: jsonWriteResponse.message.recordId, + target: alice.id, + }); + + expect (record.dataSize) > DwnConstant.maxDataSizeAllowedToBeEncoded; + + // The first invocation should succeed. + await expect(record.data.json()).to.eventually.deep.equal(dataJson); + // Assert that the second call to record.data.json() is rejected due to the timeout. + await chai.assert.isRejected(Promise.race([record.data.json(), createTimeoutPromise(5)])); + }); + + it('should return JSON once when instantiated without data', async () => { + const record = new Record(web5.dwn, { + author: alice.id, + descriptor: jsonWriteResponse.message.descriptor, + recordId: jsonWriteResponse.message.recordId, + target: alice.id, + }); + + expect (record.dataSize) > DwnConstant.maxDataSizeAllowedToBeEncoded; + + // The first invocation should succeed. + await expect(record.data.json()).to.eventually.deep.equal(dataJson); + // Assert that the second call to record.data.json() is rejected due to the timeout. + await chai.assert.isRejected(Promise.race([record.data.json(), createTimeoutPromise(5)])); + }); + }); + }); + }); +}); diff --git a/tests/data/did-documents.js b/tests/fixtures/did-documents.js similarity index 99% rename from tests/data/did-documents.js rename to tests/fixtures/did-documents.js index 36249f36f..97fc2fc34 100644 --- a/tests/data/did-documents.js +++ b/tests/fixtures/did-documents.js @@ -343,4 +343,4 @@ export const key = { ], }, }, -}; \ No newline at end of file +}; diff --git a/tests/storage/memory-storage.spec.js b/tests/storage/memory-storage.spec.js index 9d41edbc0..e77579a75 100644 --- a/tests/storage/memory-storage.spec.js +++ b/tests/storage/memory-storage.spec.js @@ -88,4 +88,4 @@ describe('MemoryStorage', async () => { valueInCache = await storage.get('key3'); expect(valueInCache).to.be.undefined; }); -}); \ No newline at end of file +}); diff --git a/tests/test-utils/promises.js b/tests/test-utils/promises.js new file mode 100644 index 000000000..a59c69ae8 --- /dev/null +++ b/tests/test-utils/promises.js @@ -0,0 +1,20 @@ +/** + * This utility function is used in tests when validating that a Promise is expected to never fulfill. + * Rather than waiting indefinitely for the Promise to fulfill, Promise.race is used with `timeoutPromise` + * to assert that the method/function being tested won't fulfill before `timeoutPromise` does. + * + * @example + * const neverFulfillingPromise = new Promise((resolve, reject) => { + * // Intentionally not resolving or rejecting the promise. + * }); + * + * // Assert that the neverFulfillingPromise is rejected due to the timeout. + * await chai.assert.isRejected(Promise.race([neverFulfillingPromise, timeoutPromise])); + */ +export function createTimeoutPromise(ms) { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Promise should not have been fulfilled')); + }, ms); + }); +} \ No newline at end of file diff --git a/tests/test-utils/test-data-generator.js b/tests/test-utils/test-data-generator.js new file mode 100644 index 000000000..a1731eaf6 --- /dev/null +++ b/tests/test-utils/test-data-generator.js @@ -0,0 +1,54 @@ +import * as ion from '../../src/did/methods/ion.js'; +import { Jws, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; + +export class TestDataGenerator { + + static async generateDid() { + return ion.create(); + } + + static generateJson(sizeBytes) { + const itemCount = sizeBytes/1024; + const items = []; + + for (let i = 0; i < itemCount; i++) { + items.push({ + id: i + 1, + name: `Item ${i + 1}`, + description: `This is a description for item`.padEnd(936, ' '), + value: 1000, + tags: ['tag1', 'tag2', 'tag3'], + }); + } + items.push({ id: 123456789 }); + + return { items }; + } + + /** + * Generates a random alpha-numeric string. + */ + static randomString(length) { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + // pick characters randomly + let randomString = ''; + for (let i = 0; i < length; i++) { + randomString += charset.charAt(Math.floor(Math.random() * charset.length)); + } + + return randomString; + }; + + /** + * Generates a random byte array of given length. + */ + static randomBytes(length) { + const randomBytes = new Uint8Array(length); + for (let i = 0; i < length; i++) { + randomBytes[i] = Math.floor(Math.random() * 256); + } + + return randomBytes; + }; +} diff --git a/tests/test-utils/test-dwn.js b/tests/test-utils/test-dwn.js new file mode 100644 index 000000000..6f1f2c207 --- /dev/null +++ b/tests/test-utils/test-dwn.js @@ -0,0 +1,47 @@ +import { Dwn, DataStoreLevel, DidResolver, EventLogLevel, MessageStoreLevel } from '@tbd54566975/dwn-sdk-js'; + +export class TestDwn { + didResolver; + dataStore; + eventLog; + messageStore; + node; + + constructor(options) { + this.node = options.node; + this.didResolver = options.didResolver; + this.dataStore = options.dataStore; + this.eventLog = options.eventLog; + this.messageStore = options.messageStore; + } + + static async create() { + const didResolver = new DidResolver(); + const dataStore = new DataStoreLevel({ + blockstoreLocation : 'test-data/DATASTORE', + }); + const eventLog = new EventLogLevel({ + location : 'test-data/EVENTLOG', + }); + const messageStore = new MessageStoreLevel({ + blockstoreLocation : 'test-data/MESSAGESTORE', + indexLocation : 'test-data/INDEX', + }); + const node = await Dwn.create({ dataStore, didResolver, eventLog, messageStore }); + return new TestDwn({ dataStore, didResolver, eventLog, messageStore, node }); + } + + async clear() { + await this.dataStore.clear(); + await this.eventLog.clear(); + await this.messageStore.clear(); + } + + async close() { + await this.node.close(); + } + + async open() { + await this.node.open(); + } +} diff --git a/tests/utils.spec.js b/tests/utils.spec.js index e19fc1df3..6f79f4feb 100644 --- a/tests/utils.spec.js +++ b/tests/utils.spec.js @@ -2,8 +2,8 @@ import { expect } from 'chai'; import * as utils from '../src/utils.js'; -describe('Utils Tests', () => { - describe('dataToBytes', () => { +describe('Web5 Utils', () => { + describe('dataToBytes()', () => { it('sets dataFormat to text/plain if string is provided', () => { const { dataFormat } = utils.dataToBytes('hello'); @@ -29,7 +29,7 @@ describe('Utils Tests', () => { }); }); - describe('isUnsignedMessage', () => { + describe('isUnsignedMessage()', () => { const signedMessage = { message: { authorization: 'value' }, }; @@ -50,7 +50,7 @@ describe('Utils Tests', () => { }); }); - describe('pascalToKebabCase', () => { + describe('pascalToKebabCase()', () => { it('should return kebab case from regular pascal case', () => { const result = utils.pascalToKebabCase('MyClassName'); expect(result).to.equal('my-class-name'); @@ -71,4 +71,4 @@ describe('Utils Tests', () => { expect(result).to.equal('my-did-class'); }); }); -}); \ No newline at end of file +});