diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 56f45d086..e6f04d154 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -383,7 +383,7 @@ export class Record implements RecordModel { // RecordsWriteMessage properties. this._attestation = options.attestation; this._authorization = options.authorization; - this._contextId = options.contextId; + this._contextId = options.contextId ?? options.initialWrite?.contextId; this._descriptor = options.descriptor; this._encryption = options.encryption; this._initialWrite = options.initialWrite; diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 25a83505a..409a1d63d 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -833,13 +833,28 @@ describe('DwnApi', () => { }); describe('records.delete()', () => { + beforeEach(async() => { + // Configure the protocol on both DWNs + const { status: aliceProtocolStatus, protocol: aliceProtocol } = await dwnAlice.protocols.configure({ message: { definition: protocolDefinition } }); + expect(aliceProtocolStatus.code).to.equal(202); + expect(aliceProtocol).to.exist; + const { status: aliceProtocolSendStatus } = await aliceProtocol.send(aliceDid.uri); + expect(aliceProtocolSendStatus.code).to.equal(202); + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ message: { definition: protocolDefinition } }); + expect(bobProtocolStatus.code).to.equal(202); + expect(bobProtocol).to.exist; + const { status: bobProtocolSendStatus } = await bobProtocol!.send(bobDid.uri); + expect(bobProtocolSendStatus.code).to.equal(202); + }); + describe('agent', () => { it('deletes a record', async () => { const { status: writeStatus, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { - schema : 'foo/bar', - dataFormat : 'text/plain' + protocol : protocolUri, + protocolPath : 'thread', + schema : protocolDefinition.types.thread.schema, } }); @@ -851,7 +866,8 @@ describe('DwnApi', () => { expect(status.code).to.equal(202); const deleteResult = await dwnAlice.records.delete({ - message: { + protocol : protocolUri, + message : { recordId: record!.id } }); @@ -950,12 +966,39 @@ describe('DwnApi', () => { it('returns a 404 when the specified record does not exist', async () => { let deleteResult = await dwnAlice.records.delete({ - message: { + protocol : protocolUri, + message : { recordId: 'abcd1234' } }); expect(deleteResult.status.code).to.equal(404); }); + + it('stores a deleted record along with its initialWrite', async () => { + // Write a record but do not store it + const { status: initialWriteStatus, record: initialWriteRecord } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + protocol : protocolUri, + protocolPath : 'thread', + schema : protocolDefinition.types.thread.schema, + } + }); + expect(initialWriteStatus.code).to.equal(202); + + // Delete the record without storing it + const { status: deleteStatus } = await initialWriteRecord.delete({ store: false }); + expect(deleteStatus.code).to.equal(202); + + // delete the record storing it + const { status: deleteStoreStatus } = await initialWriteRecord.delete(); + expect(deleteStoreStatus.code).to.equal(202); + + // try deleting it again + const { status: deleteStatus2 } = await initialWriteRecord.delete(); + expect(deleteStatus2.code).to.equal(404); + }); }); describe('from: did', () => { @@ -1808,12 +1851,20 @@ describe('DwnApi', () => { it('subscribes to records from remote', async () => { // configure a protocol const protocolConfigure = await dwnAlice.protocols.configure({ - message: { definition: { ...emailProtocolDefinition, published: true } } + message: { definition: { ...protocolDefinition, published: true } } }); expect(protocolConfigure.status.code).to.equal(202); const protocolSend = await protocolConfigure.protocol.send(aliceDid.uri); expect(protocolSend.status.code).to.equal(202); + //configure the protocol on bob's DWN + const protocolConfigureBob = await dwnBob.protocols.configure({ + message: { definition: { ...protocolDefinition, published: true } } + }); + expect(protocolConfigureBob.status.code).to.equal(202); + const protocolSendBob = await protocolConfigureBob.protocol.send(bobDid.uri); + expect(protocolSendBob.status.code).to.equal(202); + // subscribe to all messages from the protocol const records: Map = new Map(); const subscriptionHandler = async (record: Record) => { @@ -1824,7 +1875,7 @@ describe('DwnApi', () => { from : aliceDid.uri, message : { filter: { - protocol: emailProtocolDefinition.protocol + protocol: protocolUri, } }, subscriptionHandler @@ -1836,9 +1887,9 @@ describe('DwnApi', () => { data : 'Hello, world!', message : { recipient : bobDid.uri, - protocol : emailProtocolDefinition.protocol, + protocol : protocolUri, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema, + schema : protocolDefinition.types.thread.schema, dataFormat : 'text/plain' } }); @@ -1872,9 +1923,9 @@ describe('DwnApi', () => { data : 'Hello, world!', message : { recipient : bobDid.uri, - protocol : emailProtocolDefinition.protocol, + protocol : protocolUri, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema, + schema : protocolDefinition.types.thread.schema, dataFormat : 'text/plain' } }); diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 4a46ccc0c..268bb4dbb 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -1,5 +1,5 @@ import type { BearerDid ,PortableDid } from '@web5/dids'; -import type { DwnMessageParams, DwnProtocolDefinition, DwnPublicKeyJwk, DwnSigner, PortableIdentity } from '@web5/agent'; +import type { DwnMessageParams, DwnProtocolDefinition, DwnPublicKeyJwk, DwnSigner } from '@web5/agent'; import sinon from 'sinon'; import { expect } from 'chai'; @@ -17,7 +17,7 @@ import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' // NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage // Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule import { webcrypto } from 'node:crypto'; -import { Jws, Message } from '@tbd54566975/dwn-sdk-js'; +import { Jws, Message, Poller } from '@tbd54566975/dwn-sdk-js'; import { Web5 } from '../src/web5.js'; // @ts-ignore if (!globalThis.crypto) globalThis.crypto = webcrypto; @@ -33,7 +33,7 @@ describe('Record', () => { let dwnAlice: DwnApi; let dwnBob: DwnApi; let testHarness: PlatformAgentTestHarness; - let protocolUri: string; + let protocolDefinition: DwnProtocolDefinition; let consoleWarn; @@ -77,12 +77,10 @@ describe('Record', () => { await testHarness.dwnResumableTaskStore.clear(); testHarness.dwnStores.clear(); - - // give the protocol a random URI on each run - protocolUri = `http://example.com/protocol/${TestDataGenerator.randomString(15)}`; - const protocolDefinition = { + protocolDefinition = { ...emailProtocolDefinition, - protocol: protocolUri + protocol : `http://email-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}`, + published : true }; // Configure the protocol on both DWNs @@ -455,7 +453,7 @@ describe('Record', () => { data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000), message : { recipient : bobDid.uri, - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', schema : 'http://email-protocol.xyz/schema/thread', } @@ -469,7 +467,7 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', } } @@ -482,7 +480,7 @@ describe('Record', () => { from : aliceDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', } } @@ -504,7 +502,7 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', } } @@ -538,7 +536,7 @@ describe('Record', () => { from : aliceDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', } } @@ -561,7 +559,7 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', } } @@ -611,9 +609,9 @@ describe('Record', () => { }; // RecordsWriteDescriptor properties that can be pre-defined - const protocol = protocolUri; + const protocol = protocolDefinition.protocol; const protocolPath = 'thread'; - const schema = emailProtocolDefinition.types.thread.schema; + const schema = protocolDefinition.types.thread.schema; const recipient = aliceDid.uri; const published = true; @@ -1532,11 +1530,6 @@ describe('Record', () => { await testHarnessCarol.dwnResumableTaskStore.clear(); testHarnessCarol.dwnStores.clear(); - const protocolDefinition = { - ...emailProtocolDefinition, - protocol: protocolUri - }; - const { status: carolProtocolStatus, protocol: carolProtocol } = await dwnCarol.protocols.configure({ message: { definition: protocolDefinition } }); expect(carolProtocolStatus.code).to.equal(202); expect(carolProtocol).to.exist; @@ -1582,9 +1575,9 @@ describe('Record', () => { data : dataTextExceedingMaxSize, store : false, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); expect(status.code).to.equal(202); @@ -1641,9 +1634,9 @@ describe('Record', () => { data : dataTextExceedingMaxSize, store : false, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); expect(status.code).to.equal(202); @@ -1865,9 +1858,9 @@ describe('Record', () => { store : false, data : dataText, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); expect(aliceEmailStatus.code).to.equal(202); @@ -1893,8 +1886,8 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, - schema : emailProtocolDefinition.types.thread.schema + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.thread.schema } } }); @@ -1913,9 +1906,9 @@ describe('Record', () => { store : false, data : dataText, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); expect(aliceEmailStatus.code).to.equal(202); @@ -1940,8 +1933,8 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, - schema : emailProtocolDefinition.types.thread.schema + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.thread.schema } } }); @@ -1957,9 +1950,9 @@ describe('Record', () => { const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwnAlice.records.write({ data : dataString, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); @@ -1974,8 +1967,8 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, - schema : emailProtocolDefinition.types.thread.schema + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.thread.schema } } }); @@ -1995,9 +1988,9 @@ describe('Record', () => { store : false, data : dataString, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); @@ -2011,9 +2004,9 @@ describe('Record', () => { const queryResult = await dwnAlice.records.query({ message: { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } } }); @@ -2032,9 +2025,9 @@ describe('Record', () => { from : aliceDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } } }); @@ -2054,9 +2047,9 @@ describe('Record', () => { store : false, data : dataString, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); @@ -2070,9 +2063,9 @@ describe('Record', () => { const queryResult = await dwnAlice.records.query({ message: { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } } }); @@ -2091,9 +2084,9 @@ describe('Record', () => { from : bobDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } } }); @@ -2113,9 +2106,9 @@ describe('Record', () => { store : true, data : dataString, message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); @@ -2129,9 +2122,9 @@ describe('Record', () => { const queryResult = await dwnAlice.records.query({ message: { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } } }); @@ -2152,9 +2145,9 @@ describe('Record', () => { from : aliceDid.uri, message : { filter: { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } } }); @@ -2209,9 +2202,9 @@ describe('Record', () => { }; // RecordsWriteDescriptor properties that can be pre-defined - const protocol = protocolUri; + const protocol = protocolDefinition.protocol; const protocolPath = 'thread'; - const schema = emailProtocolDefinition.types.thread.schema; + const schema = protocolDefinition.types.thread.schema; const recipient = aliceDid.uri; const published = true; @@ -2310,9 +2303,9 @@ describe('Record', () => { const { record, status } = await dwnAlice.records.write({ data : 'Hello, world!', message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema, + schema : protocolDefinition.types.thread.schema, dataFormat : 'text/plain' } }); @@ -2633,8 +2626,8 @@ describe('Record', () => { const { status: threadStatus, record: threadRecord } = await dwnAlice.records.write({ data : 'Hello, world!', message : { - protocol : protocolUri, - schema : emailProtocolDefinition.types.thread.schema, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.thread.schema, protocolPath : 'thread' } }); @@ -2647,9 +2640,9 @@ describe('Record', () => { data : 'Hello, world!', message : { parentContextId : threadRecord.contextId, - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread/email', - schema : emailProtocolDefinition.types.email.schema + schema : protocolDefinition.types.email.schema } }); expect(emailStatus.code).to.equal(202); @@ -2839,6 +2832,42 @@ describe('Record', () => { }); describe('delete()', () => { + let notesProtocol: DwnProtocolDefinition; + + beforeEach(async () => { + const protocolUri = `http://example.com/notes-${TestDataGenerator.randomString(15)}`; + + notesProtocol = { + published : true, + protocol : protocolUri, + types : { + note: { + schema: 'http://example.com/note' + } + }, + structure: { + note: { + $actions: [{ + who : 'anyone', + can : ['create', 'update', 'delete'] + }] + } + } + }; + + // alice and bob both configure the protocol + const { status: aliceConfigStatus, protocol: aliceNotesProtocol } = await dwnAlice.protocols.configure({ message: { definition: notesProtocol } }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceNotesProtocolSend } = await aliceNotesProtocol.send(aliceDid.uri); + expect(aliceNotesProtocolSend.code).to.equal(202); + + const { status: bobConfigStatus, protocol: bobNotesProtocol } = await dwnBob.protocols.configure({ message: { definition: notesProtocol } }); + expect(bobConfigStatus.code).to.equal(202); + const { status: bobNotesProtocolSend } = await bobNotesProtocol!.send(bobDid.uri); + expect(bobNotesProtocolSend.code).to.equal(202); + + }); + it('deletes a local record on the local DWN', async () => { const { status: writeStatus, record } = await dwnAlice.records.write({ data : 'Hello, world!', @@ -3261,46 +3290,99 @@ describe('Record', () => { } }); - xit('signs a deleted message as owner'); + it('deletes a record from someone else', async () => { + // subscribe to records so that we can receive a record in a deleted state + const records = new Map(); + const subscriptionHandler = (record: Record) => { + records.set(record.id, record); + }; + + const { status, subscription } = await dwnAlice.records.subscribe({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol: notesProtocol.protocol, + } + }, + subscriptionHandler + }); + expect(status.code).to.equal(200, 'subscribe'); + + // bob writes a record for alice, alice deletes it and stores it + const { status: bobWriteStatus, record: bobWriteRecord } = await dwnBob.records.write({ + data : 'Hello, world!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain' + } + }); + expect(bobWriteStatus.code).to.equal(202, 'write'); + + // send the record to alice's DWN + const { status: recordSend } = await bobWriteRecord.send(aliceDid.uri); + expect(recordSend.code).to.equal(202, 'send'); + + // wait for the record to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(records.size).to.equal(1); + const record = records.get(bobWriteRecord.id); + expect(record.toJSON()).to.deep.equal(bobWriteRecord.toJSON()); + }); + + // delete the record + const bobsRecordToDelete = records.get(bobWriteRecord.id); + expect(bobsRecordToDelete.deleted).to.be.false; + + const { status: storeStatus } = await bobsRecordToDelete.delete(); + expect(storeStatus.code).to.equal(202); + expect(bobsRecordToDelete.deleted).to.be.true; + + await subscription.close(); + }); }); describe('store()', () => { - let ownerOnlyProtocolUri: string; - // install a protocol that only the owner of the DWN should be able to writing records - const ownerOnlyProtocolDefinition: DwnProtocolDefinition = { - protocol : 'owner-only', - published : true, - types : { - note: { - schema : 'http://example.com/note', - dataFormats : ['text/plain', 'application/json'] - } - }, - structure: { - note: {} - } - }; + let notesProtocol: DwnProtocolDefinition; beforeEach(async () => { - - // give the protocol a random URI on each run - ownerOnlyProtocolUri = `http://example.com/protocol/${TestDataGenerator.randomString(15)}`; - const protocolDefinition = { - ...ownerOnlyProtocolDefinition, - protocol: ownerOnlyProtocolUri + const protocolUri = `http://example.com/notes-${TestDataGenerator.randomString(15)}`; + notesProtocol = { + published : true, + protocol : protocolUri, + types : { + note: { + schema: 'http://example.com/note' + }, + request: { + schema: 'http://example.com/request' + }, + }, + structure: { + request: { + $actions: [{ + who : 'anyone', + can : ['create', 'update', 'delete'] + }] + }, + note: { + } + } }; + // alice and bob both configure the protocol + const { status: aliceConfigStatus, protocol: aliceNotesProtocol } = await dwnAlice.protocols.configure({ message: { definition: notesProtocol } }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceNotesProtocolSend } = await aliceNotesProtocol.send(aliceDid.uri); + expect(aliceNotesProtocolSend.code).to.equal(202); - const { status: aliceProtocolStatus, protocol: aliceProtocol } = await dwnAlice.protocols.configure({ message: { definition: protocolDefinition } }); - expect(aliceProtocolStatus.code).to.equal(202); - expect(aliceProtocol).to.exist; - const { status: aliceProtocolSendStatus } = await aliceProtocol.send(aliceDid.uri); - expect(aliceProtocolSendStatus.code).to.equal(202); - const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ message: { definition: protocolDefinition } }); - expect(bobProtocolStatus.code).to.equal(202); - expect(bobProtocol).to.exist; - const { status: bobProtocolSendStatus } = await bobProtocol!.send(bobDid.uri); - expect(bobProtocolSendStatus.code).to.equal(202); + const { status: bobConfigStatus, protocol: bobNotesProtocol } = await dwnBob.protocols.configure({ message: { definition: notesProtocol } }); + expect(bobConfigStatus.code).to.equal(202); + const { status: bobNotesProtocolSend } = await bobNotesProtocol!.send(bobDid.uri); + expect(bobNotesProtocolSend.code).to.equal(202); }); it('should store an external record if it has been imported by the dwn owner', async () => { @@ -3312,9 +3394,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : ownerOnlyProtocolUri, + protocol : notesProtocol.protocol, protocolPath : 'note', - schema : ownerOnlyProtocolDefinition.types.note.schema + schema : notesProtocol.types.note.schema } }); expect(status.code).to.equal(202, status.detail); @@ -3376,9 +3458,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : ownerOnlyProtocolUri, + protocol : notesProtocol.protocol, protocolPath : 'note', - schema : ownerOnlyProtocolDefinition.types.note.schema + schema : notesProtocol.types.note.schema } }); expect(status.code).to.equal(202, status.detail); @@ -3448,9 +3530,9 @@ describe('Record', () => { store : false, data : 'Hello, world!', message : { - protocol : ownerOnlyProtocolUri, + protocol : notesProtocol.protocol, protocolPath : 'note', - schema : ownerOnlyProtocolDefinition.types.note.schema + schema : notesProtocol.types.note.schema } }); expect(writeStatus.code).to.equal(202); @@ -3458,7 +3540,7 @@ describe('Record', () => { // delete the record without storing const { status: deleteStatus } = await record.delete({ store: false }); - expect(deleteStatus.code).to.equal(202); + expect(deleteStatus.code).to.equal(202, 'delete not stored'); // check that the record is in a deleted state expect(record.deleted).to.be.true; @@ -3468,14 +3550,111 @@ describe('Record', () => { // store the record const { status: storeStatus } = await record.store(); - expect(storeStatus.code).to.equal(202); + expect(storeStatus.code).to.equal(202, 'delete stored'); // check that it was called once for initial write and once for the delete expect(processMessageSpy.callCount).to.equal(2); }); + + it('stores as owner a deleted record to the local DWN from an external signer', async () => { + // subscribe to records so that we can receive a record in a deleted state + const records = new Map(); + const subscriptionHandler = (record: Record) => { + records.set(record.id, record); + }; + + const { status, subscription } = await dwnAlice.records.subscribe({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol : notesProtocol.protocol, + protocolPath : 'request' + } + }, + subscriptionHandler + }); + expect(status.code).to.equal(200, 'subscribe'); + + // bob writes a record for alice, alice deletes it and stores it + const { status: bobWriteStatus, record: bobWriteRecord } = await dwnBob.records.write({ + data : 'Hello, world!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocol.protocol, + protocolPath : 'request', + schema : notesProtocol.types.request.schema + } + }); + expect(bobWriteStatus.code).to.equal(202, 'write'); + + const { status: bobDeleteStatus } = await bobWriteRecord.delete(); + expect(bobDeleteStatus.code).to.equal(202, 'delete'); + + // send the deleted record to alice's DWN + const { status: deletedSend } = await bobWriteRecord.send(aliceDid.uri); + expect(deletedSend.code).to.equal(202, 'send'); + + // wait for the deleted record to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(records.size).to.equal(1); + const record = records.get(bobWriteRecord.id); + expect(record.deleted).to.be.true; + expect(record.toJSON()).to.deep.equal(bobWriteRecord.toJSON()); + }); + + // import the deleted record + const bobsRecordToDelete = records.get(bobWriteRecord.id); + expect(bobsRecordToDelete.deleted).to.be.true; + + const { status: storeStatus } = await bobsRecordToDelete.store(true); + expect(storeStatus.code).to.equal(202); + + await subscription.close(); + }); }); describe('import()', () => { + let notesProtocol: DwnProtocolDefinition; + + beforeEach(async () => { + const protocolUri = `https://example.com/protocol/${TestDataGenerator.randomString(15)}`; + notesProtocol = { + published : true, + protocol : protocolUri, + types : { + note: { + schema: 'http://example.com/note' + }, + request: { + schema: 'http://example.com/request' + } + }, + structure: { + request: { + $actions: [{ + who : 'anyone', + can : ['create', 'update', 'delete'] + }] + }, + note: { + } + } + }; + + // alice and bob both configure the protocol + const { status: aliceConfigStatus, protocol: aliceNotesProtocol } = await dwnAlice.protocols.configure({ message: { definition: notesProtocol } }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceNotesProtocolSend } = await aliceNotesProtocol.send(aliceDid.uri); + expect(aliceNotesProtocolSend.code).to.equal(202); + + const { status: bobConfigStatus, protocol: bobNotesProtocol } = await dwnBob.protocols.configure({ message: { definition: notesProtocol } }); + expect(bobConfigStatus.code).to.equal(202); + const { status: bobNotesProtocolSend } = await bobNotesProtocol!.send(bobDid.uri); + expect(bobNotesProtocolSend.code).to.equal(202); + + }); + it('should import an external record without storing it', async () => { // Scenario: Alice creates a record. // Bob queries for the record from Alice's DWN and then imports it without storing @@ -3486,9 +3665,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : protocolUri, - protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, } }); expect(status.code).to.equal(202, status.detail); @@ -3535,9 +3714,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : protocolUri, - protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, } }); expect(status.code).to.equal(202, status.detail); @@ -3578,41 +3757,63 @@ describe('Record', () => { expect(storedRecord.id).to.equal(record.id); }); - it('imports a deleted record to the local DWN along with the initial write', async () => { + it('signs and imports a deleted record as the owner', async () => { + // subscribe to records so that we can receive a record in a deleted state + const records = new Map(); + const subscriptionHandler = (record: Record) => { + records.set(record.id, record); + }; - // spy on the processMessage method to confirm it is called twice by the `import()` method - // once for the initial write and once for the delete - const processMessageSpy = sinon.spy(testHarness.dwn, 'processMessage'); + // subscribe to requests + const { status, subscription } = await dwnAlice.records.subscribe({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol : notesProtocol.protocol, + protocolPath : 'request' + } + }, + subscriptionHandler + }); + expect(status.code).to.equal(200, 'subscribe'); - // create a record - const { status: writeStatus, record } = await dwnAlice.records.write({ - store : false, + // bob writes a record for alice, alice deletes it and stores it + const { status: bobWriteStatus, record: bobWriteRecord } = await dwnBob.records.write({ data : 'Hello, world!', message : { - protocol : protocolUri, - protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + recipient : aliceDid.uri, + protocol : notesProtocol.protocol, + protocolPath : 'request', + schema : notesProtocol.types.request.schema, + dataFormat : 'text/plain' } }); - expect(writeStatus.code).to.equal(202); - expect(record).to.exist; + expect(bobWriteStatus.code).to.equal(202, 'write'); - // delete the record without storing - const { status: deleteStatus } = await record.delete({ store: false }); - expect(deleteStatus.code).to.equal(202); + const { status: bobDeleteStatus } = await bobWriteRecord.delete(); + expect(bobDeleteStatus.code).to.equal(202, 'delete'); - // check that the record is in a deleted state - expect(record.deleted).to.be.true; + // send the deleted record to alice's DWN + const { status: deletedSend } = await bobWriteRecord.send(aliceDid.uri); + expect(deletedSend.code).to.equal(202, 'send'); - // dwn processMessage should not have been called yet as it hasn't been stored - expect(processMessageSpy.callCount).to.equal(0); + // wait for the deleted record to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(records.size).to.equal(1); + const record = records.get(bobWriteRecord.id); + expect(record.deleted).to.be.true; + expect(record.toJSON()).to.deep.equal(bobWriteRecord.toJSON()); + }); - // store the record - const { status: importedStatus } = await record.import(); - expect(importedStatus.code).to.equal(202); + // import the deleted record + const bobsRecordToDelete = records.get(bobWriteRecord.id); + expect(bobsRecordToDelete.deleted).to.be.true; - // check that it was called once for initial write and once for the delete - expect(processMessageSpy.callCount).to.equal(2); + const { status: importStatus } = await bobsRecordToDelete.import(); + expect(importStatus.code).to.equal(202); + + await subscription.close(); }); describe('store: false', () => { @@ -3626,9 +3827,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : protocolUri, - protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema } }); expect(status.code).to.equal(202, status.detail); @@ -3691,9 +3892,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : protocolUri, - protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema } }); expect(status.code).to.equal(202, status.detail); @@ -3748,6 +3949,67 @@ describe('Record', () => { const storedRecord = bobQueryResult.records[0]; expect(storedRecord.id).to.equal(record.id); }); + + it('signs and an external deleted record as the owner', async () => { + // subscribe to records so that we can receive a record in a deleted state + const records = new Map(); + const subscriptionHandler = (record: Record) => { + records.set(record.id, record); + }; + + const { status, subscription } = await dwnAlice.records.subscribe({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol : notesProtocol.protocol, + protocolPath : 'request' + } + }, + subscriptionHandler + }); + expect(status.code).to.equal(200, 'subscribe'); + + // bob writes a record for alice, alice deletes it and stores it + const { status: bobWriteStatus, record: bobWriteRecord } = await dwnBob.records.write({ + data : 'Hello, world!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocol.protocol, + protocolPath : 'request', + schema : notesProtocol.types.request.schema, + dataFormat : 'text/plain' + } + }); + expect(bobWriteStatus.code).to.equal(202, 'write'); + + const { status: bobDeleteStatus } = await bobWriteRecord.delete(); + expect(bobDeleteStatus.code).to.equal(202, 'delete'); + + // send the deleted record to alice's DWN + const { status: deletedSend } = await bobWriteRecord.send(aliceDid.uri); + expect(deletedSend.code).to.equal(202, 'send'); + + // wait for the deleted record to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(records.size).to.equal(1); + const record = records.get(bobWriteRecord.id); + expect(record.deleted).to.be.true; + expect(record.toJSON()).to.deep.equal(bobWriteRecord.toJSON()); + }); + + // import the deleted record + const bobsRecordToDelete = records.get(bobWriteRecord.id); + expect(bobsRecordToDelete.deleted).to.be.true; + + const { status: importStatus } = await bobsRecordToDelete.import(false); + expect(importStatus.code).to.equal(202); + + const { status: storeStatus } = await bobsRecordToDelete.store(); + expect(storeStatus.code).to.equal(202); + + await subscription.close(); + }); }); }); @@ -3757,9 +4019,9 @@ describe('Record', () => { const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); @@ -3785,9 +4047,9 @@ describe('Record', () => { data : 'Hello, world!', message : { published : true, - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); expect(status.code).to.equal(202); @@ -3824,9 +4086,9 @@ describe('Record', () => { store : false, data : 'Hello, world!', message : { - protocol : protocolUri, + protocol : protocolDefinition.protocol, protocolPath : 'thread', - schema : emailProtocolDefinition.types.thread.schema + schema : protocolDefinition.types.thread.schema } }); expect(writeStatus.code).to.equal(202);