From b919cfc9ae53d6aa906bbf7d111350a8655f1d83 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sat, 9 Dec 2023 07:29:03 -0500 Subject: [PATCH 1/4] Improve Record class test structure and add failing test for newly discovered bug Signed-off-by: Frank Hinek --- packages/api/tests/record.spec.ts | 592 ++++++++++++++++++++---------- 1 file changed, 401 insertions(+), 191 deletions(-) diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 5e9f70163..00fa1b074 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -1,5 +1,4 @@ import type { PortableDid } from '@web5/dids'; -import type { ManagedIdentity } from '@web5/agent'; import type { RecordsWriteMessage, PublicJwk as DwnPublicKeyJwk, @@ -46,13 +45,14 @@ type RecordsWriteTest = RecordsWrite & RecordsWriteMessage; let testDwnUrls: string[] = [testDwnUrl]; -describe('Record', () => { +describe.only('Record', () => { let dataText: string; let dataBlob: Blob; let dataFormat: string; - let dwn: DwnApi; - let alice: ManagedIdentity; let aliceDid: PortableDid; + let bobDid: PortableDid; + let dwnAlice: DwnApi; + let dwnBob: DwnApi; let testAgent: TestManagedAgent; before(async () => { @@ -71,16 +71,25 @@ describe('Record', () => { // Create an Agent DID. await testAgent.createAgentDid(); - // Create a new Identity to author the DWN messages. + // Create an "alice" Identity to author the DWN messages. ({ did: aliceDid } = await testAgent.createIdentity({ testDwnUrls })); - alice = await testAgent.agent.identityManager.import({ + await testAgent.agent.identityManager.import({ did : aliceDid, identity : { name: 'Alice', did: aliceDid.did }, kms : 'local' }); - // Instantiate DwnApi. - dwn = new DwnApi({ agent: testAgent.agent, connectedDid: alice.did }); + // Create a "bob" Identity to author the DWN messages. + ({ did: bobDid } = await testAgent.createIdentity({ testDwnUrls })); + await testAgent.agent.identityManager.import({ + did : bobDid, + identity : { name: 'Bob', did: bobDid.did }, + kms : 'local' + }); + + // Instantiate DwnApi for both test identities. + dwnAlice = new DwnApi({ agent: testAgent.agent, connectedDid: aliceDid.did }); + dwnBob = new DwnApi({ agent: testAgent.agent, connectedDid: bobDid.did }); }); after(async () => { @@ -90,8 +99,8 @@ describe('Record', () => { it('should retain all defined properties', async () => { // RecordOptions properties - const author = alice.did; - const target = alice.did; + const author = aliceDid.did; + const target = aliceDid.did; // Retrieve `#dwn` service entry. const [ didDwnService ] = didUtils.getServices({ didDocument: aliceDid.document, id: '#dwn' }); @@ -101,11 +110,11 @@ describe('Record', () => { const [ signingKeyIdFragment ] = didDwnService!.serviceEndpoint!.signingKeys; const [ encryptionKeyIdFragment ] = didDwnService!.serviceEndpoint!.encryptionKeys; - const signingKeyId = `${alice.did}${signingKeyIdFragment}`; + const signingKeyId = `${aliceDid.did}${signingKeyIdFragment}`; const signingKeyPair = aliceDid.keySet.verificationMethodKeys!.find(keyPair => keyPair.publicKeyJwk.kid === signingKeyIdFragment); const signingPrivateKeyJwk = signingKeyPair.privateKeyJwk; - const encryptionKeyId = `${alice.did}${encryptionKeyIdFragment}`; + const encryptionKeyId = `${aliceDid.did}${encryptionKeyIdFragment}`; const encryptionPublicKeyJwk = aliceDid.keySet.verificationMethodKeys!.find(keyPair => keyPair.publicKeyJwk.kid === encryptionKeyIdFragment)!.publicKeyJwk; // RecordsWriteMessage properties that can be pre-defined @@ -137,11 +146,11 @@ describe('Record', () => { const protocol = emailProtocolDefinition.protocol; const protocolPath = 'email'; const schema = emailProtocolDefinition.types.email.schema; - const recipient = alice.did; + const recipient = aliceDid.did; const published = true; // Install a protocol on Alice's agent connected DWN. - await dwn.protocols.configure({ + await dwnAlice.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -225,7 +234,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); @@ -245,12 +254,12 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ message: { filter: { recordId: record!.id }}}); + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }}}); expect(readRecordStatus.code).to.equal(200); @@ -270,7 +279,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); @@ -290,11 +299,11 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Query for the record that was just created. - const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({ + const { records: queryRecords, status: queryRecordStatus } = await dwnAlice.records.query({ message: { filter: { recordId: record!.id }} }); expect(queryRecordStatus.code).to.equal(200); @@ -316,12 +325,12 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }} }); @@ -345,7 +354,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); @@ -358,19 +367,19 @@ describe('Record', () => { expect(readDataBytes).to.deep.equal(inputDataBytes); }); - it('returns small data payloads after dwn.records.read()', async () => { + it('returns small data payloads after dwnAlice.records.read()', async () => { // Generate data that is less than the encoded data limit to ensure that the data will not have to be fetched // with a RecordsRead when record.data.json() is executed. const dataJson = TestDataGenerator.randomJson(500); const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ message: { filter: { recordId: record!.id }}}); + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }}}); expect(readRecordStatus.code).to.equal(200); @@ -390,7 +399,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); @@ -410,11 +419,11 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Query for the record that was just created. - const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({ + const { records: queryRecords, status: queryRecordStatus } = await dwnAlice.records.query({ message: { filter: { recordId: record!.id }} }); expect(queryRecordStatus.code).to.equal(200); @@ -436,12 +445,12 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }} }); @@ -458,13 +467,13 @@ describe('Record', () => { }); describe('data.stream()', () => { - it('returns small data payloads after dwn.records.write()', async () => { + it('returns small data payloads after dwnAlice.records.write()', async () => { // Use a data payload that is less than the encoded data limit to ensure that the data will // not have to be fetched with a RecordsRead when record.data.text() is executed. const inputDataBytes = new TextEncoder().encode(dataText500Bytes); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText500Bytes }); + const { record, status } = await dwnAlice.records.write({ data: dataText500Bytes }); expect(status.code).to.equal(202); // Confirm that the length of the data read as text matches the original input data. @@ -482,11 +491,11 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(dataText500Bytes); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText500Bytes }); + const { record, status } = await dwnAlice.records.write({ data: dataText500Bytes }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ message: { filter: { recordId: record!.id }}}); + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }}}); expect(readRecordStatus.code).to.equal(200); // Confirm that the length of the data read as text matches the original input data. @@ -504,7 +513,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(dataTextExceedingMaxSize); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataTextExceedingMaxSize }); + const { record, status } = await dwnAlice.records.write({ data: dataTextExceedingMaxSize }); expect(status.code).to.equal(202); // Confirm that the length of the data read as text matches the original input data. @@ -522,11 +531,11 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(dataTextExceedingMaxSize); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataTextExceedingMaxSize }); + const { record, status } = await dwnAlice.records.write({ data: dataTextExceedingMaxSize }); expect(status.code).to.equal(202); // Query for the record that was just created. - const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({ + const { records: queryRecords, status: queryRecordStatus } = await dwnAlice.records.query({ message: { filter: { recordId: record!.id }} }); expect(queryRecordStatus.code).to.equal(200); @@ -547,12 +556,12 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(dataTextExceedingMaxSize); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataTextExceedingMaxSize }); + const { record, status } = await dwnAlice.records.write({ data: dataTextExceedingMaxSize }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }} }); expect(readRecordStatus.code).to.equal(200); @@ -568,13 +577,13 @@ describe('Record', () => { }); describe('data.text()', () => { - it('returns small data payloads after dwn.records.write()', async () => { + it('returns small data payloads after dwnAlice.records.write()', async () => { // Generate data that is less than the encoded data limit to ensure that the data will not have to be fetched // with a RecordsRead when record.data.text() is executed. const dataText = TestDataGenerator.randomString(500); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText }); + const { record, status } = await dwnAlice.records.write({ data: dataText }); expect(status.code).to.equal(202); @@ -592,12 +601,12 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(500); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText }); + const { record, status } = await dwnAlice.records.write({ data: dataText }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ message: { filter: { recordId: record!.id }}}); + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }}}); expect(readRecordStatus.code).to.equal(200); @@ -609,13 +618,13 @@ describe('Record', () => { expect(readDataText).to.deep.equal(dataText); }); - it('returns large data payloads after dwn.records.write()', async () => { + it('returns large data payloads after dwnAlice.records.write()', async () => { // Generate data that exceeds the DWN encoded data limit to ensure that the data will have to be fetched // with a RecordsRead when record.data.text() is executed. const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText }); + const { record, status } = await dwnAlice.records.write({ data: dataText }); expect(status.code).to.equal(202); @@ -633,11 +642,11 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText }); + const { record, status } = await dwnAlice.records.write({ data: dataText }); expect(status.code).to.equal(202); // Query for the record that was just created. - const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({ + const { records: queryRecords, status: queryRecordStatus } = await dwnAlice.records.query({ message: { filter: { recordId: record!.id }} }); expect(queryRecordStatus.code).to.equal(200); @@ -657,12 +666,12 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataText }); + const { record, status } = await dwnAlice.records.write({ data: dataText }); expect(status.code).to.equal(202); // Read the record that was just created. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ message: { filter: { recordId: record!.id }} }); @@ -684,16 +693,16 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Create a large record but do NOT store it on the local, agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson, store: false }); + const { record, status } = await dwnAlice.records.write({ data: dataJson, store: false }); expect(status.code).to.equal(202); // Write the large record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Query for the record that was just created on the remote DWN. - const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({ - from : alice.did, + const { records: queryRecords, status: queryRecordStatus } = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { recordId: record!.id }} }); expect(queryRecordStatus.code).to.equal(200); @@ -704,6 +713,231 @@ describe('Record', () => { expect(queriedDataBlob.size).to.equal(inputDataBytes.length); }); + + + + + + + + + + + it('returns large data payloads of records signed by another entity after remote dwn.records.query()', async () => { + /** + * WHAT IS BEING TESTED? + * + * We are testing whether a large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) record + * authored/signed by one party (Alice) can be written to another party's DWN (Bob), and that + * recipient (Bob) is able to access the data payload. This test was added to reveal a bug + * that only surfaces when accessing the data (`record.data.*`) of a record signed by a + * different entity a `Record` instance's data, which requires fetching the data from a + * remote DWN. Since the large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) data was not + * returned with the query as `encodedData`, the `Record` instance's data is not available and + * must be fetched from the remote DWN using a `RecordsRead` message. + * + * What made this bug particularly difficult to track down is that the bug only surfaces when + * keys used to sign the record are different than the keys used to fetch the record AND both + * sets of keys are unavailable to the test Agent used by the entity that is attempting to + * fetch the record. In all of the other tests, the same test agent is used to store the keys + * for all entities (e.g., "Alice", "Bob", etc.) so the bug never surfaced. + * + * In this test, Alice is the author of the record and Bob is the recipient. Alice and Bob + * each have their own Agents, DWNs, DIDs, and keys. Alice's DWN is configured to use + * Alice's DID/keys, and Bob's DWN is configured to use Bob's DID/keys. When Alice writes a + * record to Bob's DWN, the record is signed by Alice's keys. When Bob fetches the record from + * his DWN, this test validates that the `RecordsRead` is signed by Bob's keys. + * + * SETUP STEPS: + * S1. Create a second `TestManagedAgent` that only Bob will use. + */ + const testAgentBob = await TestManagedAgent.create({ + agentClass : TestUserAgent, + agentStores : 'memory', + testDataLocation : '__TESTDATA__/AGENT_BOB' + }); + await testAgentBob.createAgentDid(); + /** + * S2. Create a "bob" Identity to author the DWN messages. + */ + ({ did: bobDid } = await testAgentBob.createIdentity({ testDwnUrls })); + await testAgentBob.agent.identityManager.import({ + did : bobDid, + identity : { name: 'Bob', did: bobDid.did }, + kms : 'local' + }); + /** + * S3. Instantiate a new `DwnApi` using Bob's test agent. + */ + dwnBob = new DwnApi({ agent: testAgentBob.agent, connectedDid: bobDid.did }); + /** + * S4. Install the email protocol to both Alice's and Bob's DWNs. + */ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ + message: { definition: emailProtocolDefinition } + }); + expect(aliceStatus.code).to.equal(202); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); + expect(alicePushStatus.code).to.equal(202); + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ + message: { + definition: emailProtocolDefinition + } + }); + expect(bobStatus.code).to.equal(202); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); + expect(bobPushStatus.code).to.equal(202); + + /** + * TEST STEPS: + * + * 1. Alice creates a record but does NOT store it her local, agent-connected DWN. + */ + const { record, status } = await dwnAlice.records.write({ + data : dataTextExceedingMaxSize, + store : false, + message : { + protocol : emailProtocolDefinition.protocol, + protocolPath : 'email', + schema : emailProtocolDefinition.types.email.schema + } + }); + expect(status.code).to.equal(202); + /** + * 2. Alice writes the record to Bob's remote DWN. + */ + const { status: sendStatus } = await record!.send(bobDid.did); + expect(sendStatus.code).to.equal(202); + /** + * 3. Bob queries his remote DWN for the record that Alice just wrote. + */ + const { records: queryRecordsFrom, status: queryRecordStatusFrom } = await dwnBob.records.query({ + from : bobDid.did, + message : { filter: { recordId: record!.id }} + }); + expect(queryRecordStatusFrom.code).to.equal(200); + /** + * 4. Validate that Bob is able to access the data payload. + */ + const recordData = await queryRecordsFrom[0].data.blob(); + expect(recordData.size).to.equal(dataTextExceedingMaxSize.length); + + // Test clean-up. + await testAgentBob.clearStorage(); + }); + + it.only('returns large data payloads of records signed by another entity after remote dwn.records.query()', async () => { + /** + * WHAT IS BEING TESTED? + * + * We are testing whether a large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) record + * authored/signed by one party (Alice) can be written to another party's DWN (Bob), and that + * recipient (Bob) is able to access the data payload. This test was added to reveal a bug + * that only surfaces when accessing the data (`record.data.*`) of a record signed by a + * different entity a `Record` instance's data, which requires fetching the data from a + * remote DWN. Since the large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) data was not + * returned with the query as `encodedData`, the `Record` instance's data is not available and + * must be fetched from the remote DWN using a `RecordsRead` message. + * + * What made this bug particularly difficult to track down is that the bug only surfaces when + * keys used to sign the record are different than the keys used to fetch the record AND both + * sets of keys are unavailable to the test Agent used by the entity that is attempting to + * fetch the record. In all of the other tests, the same test agent is used to store the keys + * for all entities (e.g., "Alice", "Bob", etc.) so the bug never surfaced. + * + * In this test, Alice is the author of the record and Bob is the recipient. Alice and Bob + * each have their own Agents, DWNs, DIDs, and keys. Alice's DWN is configured to use + * Alice's DID/keys, and Bob's DWN is configured to use Bob's DID/keys. When Alice writes a + * record to Bob's DWN, the record is signed by Alice's keys. When Bob fetches the record from + * his DWN, this test validates that the `RecordsRead` is signed by Bob's keys. + * + * SETUP STEPS: + * S1. Create a second `TestManagedAgent` that only Bob will use. + */ + const testAgentBob = await TestManagedAgent.create({ + agentClass : TestUserAgent, + agentStores : 'memory', + testDataLocation : '__TESTDATA__/AGENT_BOB' + }); + await testAgentBob.createAgentDid(); + /** + * S2. Create a "bob" Identity to author the DWN messages. + */ + ({ did: bobDid } = await testAgentBob.createIdentity({ testDwnUrls })); + await testAgentBob.agent.identityManager.import({ + did : bobDid, + identity : { name: 'Bob', did: bobDid.did }, + kms : 'local' + }); + /** + * S3. Instantiate a new `DwnApi` using Bob's test agent. + */ + dwnBob = new DwnApi({ agent: testAgentBob.agent, connectedDid: bobDid.did }); + /** + * S4. Install the email protocol to both Alice's and Bob's DWNs. + */ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ + message: { definition: emailProtocolDefinition } + }); + expect(aliceStatus.code).to.equal(202); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); + expect(alicePushStatus.code).to.equal(202); + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ + message: { + definition: emailProtocolDefinition + } + }); + expect(bobStatus.code).to.equal(202); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); + expect(bobPushStatus.code).to.equal(202); + + /** + * TEST STEPS: + * + * 1. Alice creates a record but does NOT store it her local, agent-connected DWN. + */ + const { record, status } = await dwnAlice.records.write({ + data : dataTextExceedingMaxSize, + store : false, + message : { + protocol : emailProtocolDefinition.protocol, + protocolPath : 'email', + schema : emailProtocolDefinition.types.email.schema + } + }); + expect(status.code).to.equal(202); + /** + * 2. Alice writes the record to Bob's remote DWN. + */ + const { status: sendStatus } = await record!.send(bobDid.did); + expect(sendStatus.code).to.equal(202); + /** + * 3. Bob queries his remote DWN for the record that Alice just wrote. + */ + const { records: queryRecordsFrom, status: queryRecordStatusFrom } = await dwnBob.records.query({ + from : bobDid.did, + message : { filter: { recordId: record!.id }} + }); + expect(queryRecordStatusFrom.code).to.equal(200); + /** + * 4. Validate that Bob is write the record to Alice's remote DWN. + */ + const { status: sendStatusToAlice } = await queryRecordsFrom[0]!.send(aliceDid.did); + expect(sendStatusToAlice.code).to.equal(202); + + // Test clean-up. + await testAgentBob.clearStorage(); + }); + + + + + + + + + + it('returns large data payloads after remote dwn.records.read()', async () => { /** Generate data that exceeds the DWN encoded data limit to ensure that the data will have to * be fetched with a RecordsRead when record.data.* is executed. */ @@ -711,16 +945,16 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Create a large record but do NOT store it on the local, agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson, store: false }); + const { record, status } = await dwnAlice.records.write({ data: dataJson, store: false }); expect(status.code).to.equal(202); // Write the large record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Read the record that was just created on the remote DWN. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ - from : alice.did, + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ + from : aliceDid.did, message : { filter: { recordId: record!.id }} }); expect(readRecordStatus.code).to.equal(200); @@ -741,7 +975,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the 500B record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Read the data payload as bytes. @@ -767,7 +1001,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Confirm that the size, in bytes, of the data read as JSON matches the original input data. @@ -802,7 +1036,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Consume the data stream as bytes. @@ -823,7 +1057,7 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Write the large record to agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson }); + const { record, status } = await dwnAlice.records.write({ data: dataJson }); expect(status.code).to.equal(202); // Consume the data stream as bytes. @@ -844,16 +1078,16 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Create a large record but do NOT store it on the local, agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson, store: false }); + const { record, status } = await dwnAlice.records.write({ data: dataJson, store: false }); expect(status.code).to.equal(202); // Write the large record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Read the record that was just created on the remote DWN. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ - from : alice.did, + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ + from : aliceDid.did, message : { filter: { recordId: record!.id }} }); expect(readRecordStatus.code).to.equal(200); @@ -880,16 +1114,16 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Create a large record but do NOT store it on the local, agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson, store: false }); + const { record, status } = await dwnAlice.records.write({ data: dataJson, store: false }); expect(status.code).to.equal(202); // Write the large record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Read the record that was just created on the remote DWN. - const { record: readRecord, status: readRecordStatus } = await dwn.records.read({ - from : alice.did, + const { record: readRecord, status: readRecordStatus } = await dwnAlice.records.read({ + from : aliceDid.did, message : { filter: { recordId: record!.id }} }); expect(readRecordStatus.code).to.equal(200); @@ -917,16 +1151,16 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Create a large record but do NOT store it on the local, agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson, store: false }); + const { record, status } = await dwnAlice.records.write({ data: dataJson, store: false }); expect(status.code).to.equal(202); // Write the large record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Read the record that was just created on the remote DWN. - const { records: queriedRecords, status: queriedRecordStatus } = await dwn.records.query({ - from : alice.did, + const { records: queriedRecords, status: queriedRecordStatus } = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { recordId: record!.id }} }); expect(queriedRecordStatus.code).to.equal(200); @@ -956,16 +1190,16 @@ describe('Record', () => { const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson)); // Create a large record but do NOT store it on the local, agent-connected DWN. - const { record, status } = await dwn.records.write({ data: dataJson, store: false }); + const { record, status } = await dwnAlice.records.write({ data: dataJson, store: false }); expect(status.code).to.equal(202); // Write the large record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Query for the record that was just created on the remote DWN. - const { records: queriedRecords, status: queriedRecordStatus } = await dwn.records.query({ - from : alice.did, + const { records: queriedRecords, status: queriedRecordStatus } = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { recordId: record!.id }} }); expect(queriedRecordStatus.code).to.equal(200); @@ -990,23 +1224,11 @@ describe('Record', () => { }); describe('send()', () => { - let bob: ManagedIdentity; - - beforeEach(async () => { - // Create a new Identity to author the DWN messages. - const { did: bobDid } = await testAgent.createIdentity({ testDwnUrls }); - bob = await testAgent.agent.identityManager.import({ - did : bobDid, - identity : { name: 'Bob', did: bobDid.did }, - kms : 'local' - }); - }); - it('writes small records to remote DWNs for your own DID', async () => { const dataString = 'Hello, world!'; // Alice writes a message to her agent connected DWN. - const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwn.records.write({ + const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwnAlice.records.write({ data : dataString, message : { schema: 'email', @@ -1017,7 +1239,7 @@ describe('Record', () => { expect(await aliceEmailRecord?.data.text()).to.equal(dataString); // Query Alice's agent connected DWN for `email` schema records. - const aliceAgentQueryResult = await dwn.records.query({ + const aliceAgentQueryResult = await dwnAlice.records.query({ message: { filter: { schema: 'email' @@ -1031,12 +1253,12 @@ describe('Record', () => { expect(await aliceAgentEmailRecord.data.text()).to.equal(dataString); // Attempt to write the record to Alice's remote DWN. - const { status } = await aliceEmailRecord!.send(alice.did); + const { status } = await aliceEmailRecord!.send(aliceDid.did); expect(status.code).to.equal(202); // Query Alices's remote DWN for `email` schema records. - const aliceRemoteQueryResult = await dwn.records.query({ - from : alice.did, + const aliceRemoteQueryResult = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { schema: 'email' @@ -1057,7 +1279,7 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Alice writes a message to her agent connected DWN. - const { status: aliceEmailStatus } = await dwn.records.write({ + const { status: aliceEmailStatus } = await dwnAlice.records.write({ data : dataText, message : { schema: 'email', @@ -1066,7 +1288,7 @@ describe('Record', () => { expect(aliceEmailStatus.code).to.equal(202); // Query Alice's local, agent connected DWN for `email` schema records. - const aliceAgentQueryResult = await dwn.records.query({ + const aliceAgentQueryResult = await dwnAlice.records.query({ message: { filter: { schema: 'email' @@ -1079,7 +1301,7 @@ describe('Record', () => { const [ aliceAgentEmailRecord ] = aliceAgentQueryResult!.records!; // Attempt to write the record to Alice's remote DWN. - const { status } = await aliceAgentEmailRecord!.send(alice.did); + const { status } = await aliceAgentEmailRecord!.send(aliceDid.did); expect(status.code).to.equal(202); }); @@ -1089,7 +1311,7 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Alice writes a message to her agent connected DWN. - const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwn.records.write({ + const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwnAlice.records.write({ data : dataText, message : { schema: 'email', @@ -1098,7 +1320,7 @@ describe('Record', () => { expect(aliceEmailStatus.code).to.equal(202); // Read from Alice's local, agent connected DWN for the record that was just created. - const aliceAgentReadResult = await dwn.records.read({ + const aliceAgentReadResult = await dwnAlice.records.read({ message: { filter: { recordId: aliceEmailRecord.id @@ -1110,7 +1332,7 @@ describe('Record', () => { expect(aliceAgentReadResult.record).to.exist; // Attempt to write the record to Alice's remote DWN. - const { status } = await aliceAgentReadResult.record.send(alice.did); + const { status } = await aliceAgentReadResult.record.send(aliceDid.did); expect(status.code).to.equal(202); }); @@ -1124,7 +1346,7 @@ describe('Record', () => { */ // Alice writes a message to her agent connected DWN. - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { schema : 'foo/bar', @@ -1134,7 +1356,7 @@ describe('Record', () => { expect(status.code).to.equal(202); // Write the record to Alice's remote DWN. - let sendResult = await record.send(alice.did); + let sendResult = await record.send(aliceDid.did); expect(sendResult.status.code).to.equal(202); // Update the record by mutating the data property. @@ -1142,7 +1364,7 @@ describe('Record', () => { expect(updateResult.status.code).to.equal(202); // Write the updated record to Alice's remote DWN a second time. - sendResult = await record!.send(alice.did); + sendResult = await record!.send(aliceDid.did); expect(sendResult.status.code).to.equal(202); // Update the record again. @@ -1150,14 +1372,14 @@ describe('Record', () => { expect(updateResult.status.code).to.equal(202); // Write the updated record to Alice's remote DWN a third time. - sendResult = await record!.send(alice.did); + sendResult = await record!.send(aliceDid.did); expect(sendResult.status.code).to.equal(202); }); // TODO: Fix after changes are made to dwn-sdk-js to include the initial write in every query/read response. it('fails to write updated records to a remote DWN that is missing the initial write', async () => { // Alice writes a message to her agent connected DWN. - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { schema : 'foo/bar', @@ -1171,7 +1393,7 @@ describe('Record', () => { expect(updateResult.status.code).to.equal(202); // Write the updated record to Alice's remote DWN a second time. - const sendResult = await record!.send(alice.did); + const sendResult = await record!.send(aliceDid.did); expect(sendResult.status.code).to.equal(400); expect(sendResult.status.detail).to.equal('RecordsWriteGetInitialWriteNotFound: initial write is not found'); @@ -1185,21 +1407,18 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Install the email protocol for Alice's local DWN. - let { protocol: aliceProtocol, status: aliceStatus } = await dwn.protocols.configure({ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ message: { definition: emailProtocolDefinition } }); expect(aliceStatus.code).to.equal(202); expect(aliceProtocol).to.exist; // Install the email protocol for Alice's remote DWN. - const { status: alicePushStatus } = await aliceProtocol!.send(alice.did); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); expect(alicePushStatus.code).to.equal(202); - // Instantiate DwnApi instance for Bob. - const bobDwn = new DwnApi({ agent: testAgent.agent, connectedDid: bob.did }); - // Install the email protocol for Bob's local DWN. - const { protocol: bobProtocol, status: bobStatus } = await bobDwn.protocols.configure({ + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1209,11 +1428,11 @@ describe('Record', () => { expect(bobProtocol).to.exist; // Install the email protocol for Bob's remote DWN. - const { status: bobPushStatus } = await bobProtocol!.send(bob.did); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); expect(bobPushStatus.code).to.equal(202); // Alice creates a new large record but does not store it in her local DWN. - const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwn.records.write({ + const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwnAlice.records.write({ store : false, data : dataText, message : { @@ -1225,24 +1444,24 @@ describe('Record', () => { expect(aliceEmailStatus.code).to.equal(202); // Alice writes the large record to her own remote DWN. - const { status: sendStatus } = await aliceEmailRecord!.send(alice.did); + const { status: sendStatus } = await aliceEmailRecord!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Alice queries for the record that was just created on her remote DWN. - const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({ - from : alice.did, + const { records: queryRecords, status: queryRecordStatus } = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { recordId: aliceEmailRecord!.id }} }); expect(queryRecordStatus.code).to.equal(200); // Attempt to write the record to Bob's DWN. const [ queryRecord ] = queryRecords; - const { status } = await queryRecord!.send(bob.did); + const { status } = await queryRecord!.send(bobDid.did); expect(status.code).to.equal(202); // Confirm Bob can query his own remote DWN for the created record. - const bobQueryResult = await bobDwn.records.query({ - from : bob.did, + const bobQueryResult = await dwnBob.records.query({ + from : bobDid.did, message : { filter: { schema: 'http://email-protocol.xyz/schema/email' @@ -1260,21 +1479,18 @@ describe('Record', () => { const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); // Install the email protocol for Alice's local DWN. - let { protocol: aliceProtocol, status: aliceStatus } = await dwn.protocols.configure({ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ message: { definition: emailProtocolDefinition } }); expect(aliceStatus.code).to.equal(202); expect(aliceProtocol).to.exist; // Install the email protocol for Alice's remote DWN. - const { status: alicePushStatus } = await aliceProtocol!.send(alice.did); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); expect(alicePushStatus.code).to.equal(202); - // Instantiate DwnApi instance for Bob. - const bobDwn = new DwnApi({ agent: testAgent.agent, connectedDid: bob.did }); - // Install the email protocol for Bob's local DWN. - const { protocol: bobProtocol, status: bobStatus } = await bobDwn.protocols.configure({ + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1284,11 +1500,11 @@ describe('Record', () => { expect(bobProtocol).to.exist; // Install the email protocol for Bob's remote DWN. - const { status: bobPushStatus } = await bobProtocol!.send(bob.did); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); expect(bobPushStatus.code).to.equal(202); // Alice creates a new large record but does not store it in her local DWN. - const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwn.records.write({ + const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwnAlice.records.write({ store : false, data : dataText, message : { @@ -1300,23 +1516,23 @@ describe('Record', () => { expect(aliceEmailStatus.code).to.equal(202); // Alice writes the large record to her own remote DWN. - const { status: sendStatus } = await aliceEmailRecord!.send(alice.did); + const { status: sendStatus } = await aliceEmailRecord!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Alice queries for the record that was just created on her remote DWN. - const { record: queryRecord, status: queryRecordStatus } = await dwn.records.read({ - from : alice.did, + const { record: queryRecord, status: queryRecordStatus } = await dwnAlice.records.read({ + from : aliceDid.did, message : { filter: { recordId: aliceEmailRecord!.id }} }); expect(queryRecordStatus.code).to.equal(200); // Attempt to write the record to Bob's DWN. - const { status } = await queryRecord!.send(bob.did); + const { status } = await queryRecord!.send(bobDid.did); expect(status.code).to.equal(202); // Confirm Bob can query his own remote DWN for the created record. - const bobQueryResult = await bobDwn.records.query({ - from : bob.did, + const bobQueryResult = await dwnBob.records.query({ + from : bobDid.did, message : { filter: { schema: 'http://email-protocol.xyz/schema/email' @@ -1332,7 +1548,7 @@ describe('Record', () => { const dataString = 'Hello, world!'; // Install the email protocol for Alice's local DWN. - let { protocol: aliceProtocol, status: aliceStatus } = await dwn.protocols.configure({ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1342,14 +1558,11 @@ describe('Record', () => { expect(aliceProtocol).to.exist; // Install the email protocol for Alice's remote DWN. - const { status: alicePushStatus } = await aliceProtocol!.send(alice.did); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); expect(alicePushStatus.code).to.equal(202); - // Instantiate DwnApi instance for Bob. - const bobDwn = new DwnApi({ agent: testAgent.agent, connectedDid: bob.did }); - // Install the email protocol for Bob's local DWN. - const { protocol: bobProtocol, status: bobStatus } = await bobDwn.protocols.configure({ + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1359,11 +1572,11 @@ describe('Record', () => { expect(bobProtocol).to.exist; // Install the email protocol for Bob's remote DWN. - const { status: bobPushStatus } = await bobProtocol!.send(bob.did); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); expect(bobPushStatus.code).to.equal(202); // Alice writes a message to her own DWN. - const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwn.records.write({ + const { status: aliceEmailStatus, record: aliceEmailRecord } = await dwnAlice.records.write({ data : dataString, message : { protocol : emailProtocolDefinition.protocol, @@ -1375,12 +1588,12 @@ describe('Record', () => { expect(aliceEmailStatus.code).to.equal(202); // Attempt to write the message to Bob's DWN. - const { status } = await aliceEmailRecord!.send(bob.did); + const { status } = await aliceEmailRecord!.send(bobDid.did); expect(status.code).to.equal(202); // Query Bob's remote DWN for `email` schema records. - const bobQueryResult = await bobDwn.records.query({ - from : bob.did, + const bobQueryResult = await dwnBob.records.query({ + from : bobDid.did, message : { filter: { schema: 'http://email-protocol.xyz/schema/email' @@ -1399,7 +1612,7 @@ describe('Record', () => { it('writes records to your own remote DWN but not your local DWN', async () => { // Alice creates a record but does not store it on her local DWN with `store: false`. const dataString = 'Hello, world!'; - const writeResult = await dwn.records.write({ + const writeResult = await dwnAlice.records.write({ store : false, data : dataString, message : { @@ -1414,7 +1627,7 @@ describe('Record', () => { expect(await writeResult.record?.data.text()).to.equal(dataString); // Query Alice's agent DWN for `text/plain` records. - const queryResult = await dwn.records.query({ + const queryResult = await dwnAlice.records.query({ message: { filter: { dataFormat: 'text/plain' @@ -1428,12 +1641,12 @@ describe('Record', () => { expect(queryResult.records!.length).to.equal(0); // Alice writes the message to her remote DWN. - const { status } = await writeResult.record!.send(alice.did); + const { status } = await writeResult.record!.send(aliceDid.did); expect(status.code).to.equal(202); // Query Alice's remote DWN for `plain/text` records. - const aliceRemoteQueryResult = await dwn.records.query({ - from : alice.did, + const aliceRemoteQueryResult = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { dataFormat: 'text/plain' @@ -1451,7 +1664,7 @@ describe('Record', () => { it(`writes records to someone else's remote DWN but not your agent DWN`, async () => { // Install a protocol on Alice's agent connected DWN. - let { protocol: aliceProtocol, status: aliceStatus } = await dwn.protocols.configure({ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1461,14 +1674,11 @@ describe('Record', () => { expect(aliceProtocol).to.exist; // Install the protocol on Alice's remote DWN. - const { status: alicePushStatus } = await aliceProtocol!.send(alice.did); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); expect(alicePushStatus.code).to.equal(202); - // Instantiate DwnApi instance for Bob. - const bobDwn = new DwnApi({ agent: testAgent.agent, connectedDid: bob.did }); - // Install the email protocol for Bob's local DWN. - const { protocol: bobProtocol, status: bobStatus } = await bobDwn.protocols.configure({ + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1478,12 +1688,12 @@ describe('Record', () => { expect(bobProtocol).to.exist; // Install the email protocol for Bob's remote DWN. - const { status: bobPushStatus } = await bobProtocol!.send(bob.did); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); expect(bobPushStatus.code).to.equal(202); // Alice writes a message to her agent DWN with `store: false`. const dataString = 'Hello, world!'; - const writeResult = await dwn.records.write({ + const writeResult = await dwnAlice.records.write({ store : false, data : dataString, message : { @@ -1500,7 +1710,7 @@ describe('Record', () => { expect(await writeResult.record?.data.text()).to.equal(dataString); // Query Alice's agent DWN for `email` schema records. - const queryResult = await dwn.records.query({ + const queryResult = await dwnAlice.records.query({ message: { filter: { schema: 'http://email-protocol.xyz/schema/email' @@ -1514,12 +1724,12 @@ describe('Record', () => { expect(queryResult.records!.length).to.equal(0); // Alice writes the message to Bob's remote DWN. - const { status } = await writeResult.record!.send(bob.did); + const { status } = await writeResult.record!.send(bobDid.did); expect(status.code).to.equal(202); // Query Bobs's remote DWN for `email` schema records. - const bobQueryResult = await bobDwn.records.query({ - from : bob.did, + const bobQueryResult = await dwnBob.records.query({ + from : bobDid.did, message : { filter: { dataFormat: 'text/plain' @@ -1538,7 +1748,7 @@ describe('Record', () => { it('has no effect if `store: true`', async () => { // Alice writes a message to her agent DWN with `store: true`. const dataString = 'Hello, world!'; - const writeResult = await dwn.records.write({ + const writeResult = await dwnAlice.records.write({ store : true, data : dataString, message : { @@ -1553,7 +1763,7 @@ describe('Record', () => { expect(await writeResult.record?.data.text()).to.equal(dataString); // Query Alice's agent DWN for `text/plain` records. - const queryResult = await dwn.records.query({ + const queryResult = await dwnAlice.records.query({ message: { filter: { dataFormat: 'text/plain' @@ -1569,12 +1779,12 @@ describe('Record', () => { expect(await aliceAgentRecord.data.text()).to.equal(dataString); // Alice writes the message to her remote DWN. - const { status } = await writeResult.record!.send(alice.did); + const { status } = await writeResult.record!.send(aliceDid.did); expect(status.code).to.equal(202); // Query Alice's remote DWN for `plain/text` records. - const aliceRemoteQueryResult = await dwn.records.query({ - from : alice.did, + const aliceRemoteQueryResult = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { dataFormat: 'text/plain' @@ -1595,8 +1805,8 @@ describe('Record', () => { describe('toJSON()', () => { it('should return all defined properties', async () => { // RecordOptions properties - const author = alice.did; - const target = alice.did; + const author = aliceDid.did; + const target = aliceDid.did; // Retrieve `#dwn` service entry. const [ didDwnService ] = didUtils.getServices({ didDocument: aliceDid.document, id: '#dwn' }); @@ -1606,11 +1816,11 @@ describe('Record', () => { const [ signingKeyIdFragment ] = didDwnService!.serviceEndpoint!.signingKeys; const [ encryptionKeyIdFragment ] = didDwnService!.serviceEndpoint!.encryptionKeys; - const signingKeyId = `${alice.did}${signingKeyIdFragment}`; + const signingKeyId = `${aliceDid.did}${signingKeyIdFragment}`; const signingKeyPair = aliceDid.keySet.verificationMethodKeys!.find(keyPair => keyPair.publicKeyJwk.kid === signingKeyIdFragment); const signingPrivateKeyJwk = signingKeyPair.privateKeyJwk; - const encryptionKeyId = `${alice.did}${encryptionKeyIdFragment}`; + const encryptionKeyId = `${aliceDid.did}${encryptionKeyIdFragment}`; const encryptionPublicKeyJwk = aliceDid.keySet.verificationMethodKeys!.find(keyPair => keyPair.publicKeyJwk.kid === encryptionKeyIdFragment)!.publicKeyJwk; // RecordsWriteMessage properties that can be pre-defined @@ -1642,11 +1852,11 @@ describe('Record', () => { const protocol = emailProtocolDefinition.protocol; const protocolPath = 'email'; const schema = emailProtocolDefinition.types.email.schema; - const recipient = alice.did; + const recipient = aliceDid.did; const published = true; // Install a protocol on Alice's agent connected DWN. - await dwn.protocols.configure({ + await dwnAlice.protocols.configure({ message: { definition: emailProtocolDefinition } @@ -1721,7 +1931,7 @@ describe('Record', () => { describe('update()', () => { it('updates a local record on the local DWN', async () => { - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { schema : 'foo/bar', @@ -1737,7 +1947,7 @@ describe('Record', () => { const updateResult = await record!.update({ data: 'bye' }); expect(updateResult.status.code).to.equal(202); - const readResult = await dwn.records.read({ + const readResult = await dwnAlice.records.read({ message: { filter: { recordId: record!.id @@ -1758,7 +1968,7 @@ describe('Record', () => { // TODO: Fix after changes are made to dwn-sdk-js to include the initial write in every query/read response. it('fails to update a record locally that only written to a remote DWN', async () => { // Create a record but do not store it on the local DWN. - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ store : false, data : 'Hello, world!', message : { @@ -1773,7 +1983,7 @@ describe('Record', () => { // const dataCidBeforeDataUpdate = record!.dataCid; // Write the record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); /** Attempt to update the record, which should write the updated record the local DWN but @@ -1786,7 +1996,7 @@ describe('Record', () => { // expect(updateResult.status.code).to.equal(202); // Confirm that the record was written to the local DWN. - // const readResult = await dwn.records.read({ + // const readResult = await dwnAlice.records.read({ // message: { // filter: { // recordId: record!.id @@ -1808,7 +2018,7 @@ describe('Record', () => { // TODO: Fix after changes are made to dwn-sdk-js to include the initial write in every query/read response. it('fails to update a record locally that was initially read from a remote DWN', async () => { // Create a record but do not store it on the local DWN. - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ store : false, data : 'Hello, world!', message : { @@ -1823,12 +2033,12 @@ describe('Record', () => { // const dataCidBeforeDataUpdate = record!.dataCid; // Write the record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Read the record from the remote DWN. - const readResult = await dwn.records.read({ - from : alice.did, + const readResult = await dwnAlice.records.read({ + from : aliceDid.did, message : { filter: { recordId: record!.id @@ -1847,7 +2057,7 @@ describe('Record', () => { // expect(updateResult.status.code).to.equal(202); // Confirm that the record was written to the local DWN. - // const readResult = await dwn.records.read({ + // const readResult = await dwnAlice.records.read({ // message: { // filter: { // recordId: record!.id @@ -1869,7 +2079,7 @@ describe('Record', () => { // TODO: Fix after changes are made to dwn-sdk-js to include the initial write in every query/read response. it('fails to update a record locally that was initially queried from a remote DWN', async () => { // Create a record but do not store it on the local DWN. - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ store : false, data : 'Hello, world!', message : { @@ -1884,12 +2094,12 @@ describe('Record', () => { // const dataCidBeforeDataUpdate = record!.dataCid; // Write the record to a remote DWN. - const { status: sendStatus } = await record!.send(alice.did); + const { status: sendStatus } = await record!.send(aliceDid.did); expect(sendStatus.code).to.equal(202); // Query the record from the remote DWN. - const queryResult = await dwn.records.query({ - from : alice.did, + const queryResult = await dwnAlice.records.query({ + from : aliceDid.did, message : { filter: { recordId: record!.id @@ -1910,7 +2120,7 @@ describe('Record', () => { // expect(updateResult.status.code).to.equal(202); // Confirm that the record was written to the local DWN. - // const readResult = await dwn.records.read({ + // const readResult = await dwnAlice.records.read({ // message: { // filter: { // recordId: record!.id @@ -1931,7 +2141,7 @@ describe('Record', () => { it('returns new dateModified after each update', async () => { // Initial write of the record. - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { schema : 'foo/bar', @@ -1959,7 +2169,7 @@ describe('Record', () => { }); it('throws an exception when an immutable property is modified', async () => { - const { status, record } = await dwn.records.write({ + const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { schema : 'foo/bar', From 585be3fc2bff7ad142304226680a04af197702b5 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sat, 9 Dec 2023 07:29:39 -0500 Subject: [PATCH 2/4] Rename remoteTarget to remoteOrigin Signed-off-by: Frank Hinek --- packages/api/src/dwn-api.ts | 21 ++++++++++----------- packages/api/src/record.ts | 28 ++++++++++++++-------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index b9ee7ce31..04cc47818 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -273,7 +273,6 @@ export class DwnApi { // Remove target from inherited properties since target is being explicitly defined in method parameters. delete inheritedProperties.target; - // If `data` is being updated then `dataCid` and `dataSize` must not be present. if (request.data !== undefined) { delete inheritedProperties.dataCid; @@ -379,12 +378,12 @@ export class DwnApi { */ target : this.connectedDid, /** - * If the record was returned by a query of a remote DWN, set the `remoteTarget` to - * the DID of the DWN that returned the record. The `remoteTarget` will be used to - * determine which DWN to send subsequent read requests to in the event the data payload - * exceeds the threshold for being returned with queries. + * If the record was returned by a query of a remote DWN, set the `remoteOrigin` to + * the DID of the DWN that returned the record. The `remoteOrigin` property will be used + * to determine which DWN to send subsequent read requests to in the event the data + * payload exceeds the threshold for being returned with queries. */ - remoteTarget : request.from, + remoteOrigin : request.from, ...entry as RecordsWriteMessage }; const record = new Record(this.agent, recordOptions); @@ -439,12 +438,12 @@ export class DwnApi { */ target : this.connectedDid, /** - * If the record was returned by a query of a remote DWN, set the `remoteTarget` to - * the DID of the DWN that returned the record. The `remoteTarget` will be used to - * determine which DWN to send subsequent read requests to in the event the data payload - * exceeds the threshold for being returned with queries. + * If the record was returned by reading from a remote DWN, set the `remoteOrigin` to + * the DID of the DWN that returned the record. The `remoteOrigin` property will be used + * to determine which DWN to send subsequent read requests to in the event the data + * payload must be read again (e.g., if the data stream is consumed). */ - remoteTarget : request.from, + remoteOrigin : request.from, ...responseRecord, }; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 8c742c29a..0455a4ab4 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -23,7 +23,7 @@ export type RecordOptions = RecordsWriteMessage & { target: string; encodedData?: string | Blob; data?: Readable | ReadableStream; - remoteTarget?: string; + remoteOrigin?: string; }; /** @@ -84,7 +84,7 @@ export class Record implements RecordModel { private _encryption?: RecordsWriteMessage['encryption']; private _readableStream?: Readable; private _recordId: string; - private _remoteTarget?: string; + private _remoteOrigin?: string; // Immutable DWN Record properties. @@ -155,10 +155,11 @@ export class Record implements RecordModel { this.author = options.author; this.target = options.target; - /** If the record was queried from a remote DWN, the `remoteTarget` DID will be defined. This - * value is used to send subsequent read requests to the same remote DWN in the event the - * record's data payload was too large to be returned in query results. */ - this._remoteTarget = options.remoteTarget; + /** If the record was queried or read from a remote DWN, the `remoteOrigin` DID will be + * defined. This value is used to send subsequent read requests to the same remote DWN in the + * event the record's data payload was too large to be returned in query results. or must be + * read again (e.g., if the data stream is consumed). */ + this._remoteOrigin = options.remoteOrigin; // RecordsWriteMessage properties. this._attestation = options.attestation; @@ -264,13 +265,12 @@ export class Record implements RecordModel { self._readableStream = NodeStream.fromWebReadable({ readableStream: self._encodedData.stream() }); } else if (!NodeStream.isReadable({ readable: self._readableStream })) { - /** If `encodedData` is not set, then the Record was instantiated by `dwn.records.read()` - * or was too large to be returned in `dwn.records.query()` results. In either case, the - * data is not available in-memory and must be fetched from either: */ - self._readableStream = self._remoteTarget ? - // 1. ...a remote DWN if the record was queried from a remote DWN. - await self.readRecordData({ target: self._remoteTarget, isRemote: true }) : - // 2. ...a local DWN if the record was queried from the local DWN. + /** If the data stream for this `Record` instance has already been partially or fully + * consumed, then the data must be fetched again from either: */ + self._readableStream = self._remoteOrigin ? + // A. ...a remote DWN if the record was originally queried from a remote DWN. + await self.readRecordData({ target: self._remoteOrigin, isRemote: true }) : + // B. ...a local DWN if the record was originally queried from the local DWN. await self.readRecordData({ target: self.target, isRemote: false }); } @@ -440,7 +440,7 @@ export class Record implements RecordModel { } /** - * Fetches the record's data from the source DWN. + * Fetches the record's data from the specified DWN. * * This private method is called when the record data is not available in-memory * and needs to be fetched from either a local or a remote DWN. From 23d86b74527f8251cde7d3eb0878a68ced05ac22 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sat, 9 Dec 2023 16:00:15 -0500 Subject: [PATCH 3/4] Correct typo in error message Signed-off-by: Frank Hinek --- packages/agent/src/dwn-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent/src/dwn-manager.ts b/packages/agent/src/dwn-manager.ts index d5888c11c..8f94612f5 100644 --- a/packages/agent/src/dwn-manager.ts +++ b/packages/agent/src/dwn-manager.ts @@ -225,7 +225,7 @@ export class DwnManager { dwnReply = await this.agent.rpcClient.sendDwnRequest(dwnRpcRequest as DwnRpcRequest); break; } catch(error: unknown) { - const message = (error instanceof Error) ? error.message : 'Uknown error'; + const message = (error instanceof Error) ? error.message : 'Unknown error'; errorMessages.push({ url: dwnUrl, message }); } } From 33f666a010d9564c259edf1ed645f98ed71bee44 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sat, 9 Dec 2023 16:09:44 -0500 Subject: [PATCH 4/4] Improve semantics of Record class Signed-off-by: Frank Hinek --- packages/api/src/dwn-api.ts | 19 +- packages/api/src/record.ts | 53 ++-- packages/api/tests/record.spec.ts | 438 ++++++++++++++---------------- 3 files changed, 237 insertions(+), 273 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 04cc47818..a7c0cec10 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -270,9 +270,6 @@ export class DwnApi { createFrom: async (request: RecordsCreateFromRequest): Promise => { const { author: inheritedAuthor, ...inheritedProperties } = request.record.toJSON(); - // Remove target from inherited properties since target is being explicitly defined in method parameters. - delete inheritedProperties.target; - // If `data` is being updated then `dataCid` and `dataSize` must not be present. if (request.data !== undefined) { delete inheritedProperties.dataCid; @@ -372,11 +369,11 @@ export class DwnApi { */ author : RecordsWrite.getAuthor(entry), /** - * Set the `target` DID to currently connected DID so that subsequent calls to + * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN even if the record was returned by a query of a remote DWN. */ - target : this.connectedDid, + connectedDid : this.connectedDid, /** * If the record was returned by a query of a remote DWN, set the `remoteOrigin` to * the DID of the DWN that returned the record. The `remoteOrigin` property will be used @@ -432,11 +429,11 @@ export class DwnApi { */ author : RecordsWrite.getAuthor(responseRecord), /** - * Set the `target` DID to currently connected DID so that subsequent calls to + * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN even if the record was read from a remote DWN. */ - target : this.connectedDid, + connectedDid : this.connectedDid, /** * If the record was returned by reading from a remote DWN, set the `remoteOrigin` to * the DID of the DWN that returned the record. The `remoteOrigin` property will be used @@ -489,14 +486,14 @@ export class DwnApi { * Assume the author is the connected DID since the record was just written to the * local DWN. */ - author : this.connectedDid, - encodedData : dataBlob, + author : this.connectedDid, /** - * Set the `target` DID to currently connected DID so that subsequent calls to + * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN. */ - target : this.connectedDid, + connectedDid : this.connectedDid, + encodedData : dataBlob, ...responseMessage, }; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 0455a4ab4..e162d0cd4 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -20,7 +20,7 @@ import { dataToBlob } from './utils.js'; */ export type RecordOptions = RecordsWriteMessage & { author: string; - target: string; + connectedDid: string; encodedData?: string | Blob; data?: Readable | ReadableStream; remoteOrigin?: string; @@ -37,7 +37,6 @@ export type RecordModel = RecordsWriteDescriptor & { author: string; recordId?: string; - target: string; } /** @@ -68,29 +67,29 @@ export type RecordUpdateOptions = { * @beta */ export class Record implements RecordModel { - // mutable properties - - /** Record's author DID */ - author: string; - - /** Record's target DID */ - target: string; - + // Record instance metadata. private _agent: Web5Agent; + private _connectedDid: string; + private _encodedData?: Blob; + private _readableStream?: Readable; + private _remoteOrigin?: string; + + // Private variables for DWN `RecordsWrite` message properties. + private _author: string; private _attestation?: RecordsWriteMessage['attestation']; private _contextId?: string; private _descriptor: RecordsWriteDescriptor; - private _encodedData?: Blob; private _encryption?: RecordsWriteMessage['encryption']; - private _readableStream?: Readable; private _recordId: string; - private _remoteOrigin?: string; - // Immutable DWN Record properties. + // Getters for immutable DWN Record properties. /** Record's signatures attestation */ get attestation(): RecordsWriteMessage['attestation'] { return this._attestation; } + /** DID that signed the record. */ + get author(): string { return this._author; } + /** Record's context ID */ get contextId() { return this._contextId; } @@ -127,7 +126,7 @@ export class Record implements RecordModel { /** Record's schema */ get schema() { return this._descriptor.schema; } - // Mutable DWN Record properties. + // Getters for mutable DWN Record properties. /** Record's CID */ get dataCid() { return this._descriptor.dataCid; } @@ -150,10 +149,13 @@ export class Record implements RecordModel { constructor(agent: Web5Agent, options: RecordOptions) { this._agent = agent; - /** Store the target and author DIDs that were used to create the message to use for subsequent - * updates, reads, etc. */ - this.author = options.author; - this.target = options.target; + /** Store the author DID that originally signed the message as a convenience for developers, so + * that they don't have to decode the signer's DID from the JWS. */ + this._author = options.author; + + /** Store the currently `connectedDid` so that subsequent message signing is done with the + * connected DID's keys and DWN requests target the connected DID's DWN. */ + this._connectedDid = options.connectedDid; /** If the record was queried or read from a remote DWN, the `remoteOrigin` DID will be * defined. This value is used to send subsequent read requests to the same remote DWN in the @@ -271,7 +273,7 @@ export class Record implements RecordModel { // A. ...a remote DWN if the record was originally queried from a remote DWN. await self.readRecordData({ target: self._remoteOrigin, isRemote: true }) : // B. ...a local DWN if the record was originally queried from the local DWN. - await self.readRecordData({ target: self.target, isRemote: false }); + await self.readRecordData({ target: self._connectedDid, isRemote: false }); } if (!self._readableStream) { @@ -305,7 +307,7 @@ export class Record implements RecordModel { async send(target: string): Promise { const { reply: { status } } = await this._agent.sendDwnRequest({ messageType : DwnInterfaceName.Records + DwnMethodName.Write, - author : this.author, + author : this._connectedDid, dataStream : await this.data.blob(), target : target, messageOptions : this.toJSON(), @@ -338,8 +340,7 @@ export class Record implements RecordModel { published : this.published, recipient : this.recipient, recordId : this.id, - schema : this.schema, - target : this.target, + schema : this.schema }; } @@ -415,11 +416,11 @@ export class Record implements RecordModel { }; const agentResponse = await this._agent.processDwnRequest({ - author : this.author, + author : this._connectedDid, dataStream : dataBlob, messageOptions, messageType : DwnInterfaceName.Records + DwnMethodName.Write, - target : this.target, + target : this._connectedDid, }); const { message, reply: { status } } = agentResponse; @@ -456,7 +457,7 @@ export class Record implements RecordModel { */ private async readRecordData({ target, isRemote }: { target: string, isRemote: boolean }) { const readRequest = { - author : this.author, + author : this._connectedDid, messageOptions : { filter: { recordId: this.id } }, messageType : DwnInterfaceName.Records + DwnMethodName.Read, target, diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 00fa1b074..1fdfc0a32 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -45,7 +45,7 @@ type RecordsWriteTest = RecordsWrite & RecordsWriteMessage; let testDwnUrls: string[] = [testDwnUrl]; -describe.only('Record', () => { +describe('Record', () => { let dataText: string; let dataBlob: Blob; let dataFormat: string; @@ -100,7 +100,6 @@ describe.only('Record', () => { it('should retain all defined properties', async () => { // RecordOptions properties const author = aliceDid.did; - const target = aliceDid.did; // Retrieve `#dwn` service entry. const [ didDwnService ] = didUtils.getServices({ didDocument: aliceDid.document, id: '#dwn' }); @@ -184,14 +183,13 @@ describe.only('Record', () => { // Create record using test RecordsWriteMessage. const record = new Record(testAgent.agent, { ...recordsWrite.message, - encodedData: dataBlob, - target, - author, + encodedData : dataBlob, + author : aliceDid.did, + connectedDid : aliceDid.did }); // Retained Record properties expect(record.author).to.equal(author); - expect(record.target).to.equal(target); // Retained RecordsWriteMessage top-level properties expect(record.contextId).to.equal(recordsWrite.message.contextId); @@ -713,231 +711,6 @@ describe.only('Record', () => { expect(queriedDataBlob.size).to.equal(inputDataBytes.length); }); - - - - - - - - - - - it('returns large data payloads of records signed by another entity after remote dwn.records.query()', async () => { - /** - * WHAT IS BEING TESTED? - * - * We are testing whether a large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) record - * authored/signed by one party (Alice) can be written to another party's DWN (Bob), and that - * recipient (Bob) is able to access the data payload. This test was added to reveal a bug - * that only surfaces when accessing the data (`record.data.*`) of a record signed by a - * different entity a `Record` instance's data, which requires fetching the data from a - * remote DWN. Since the large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) data was not - * returned with the query as `encodedData`, the `Record` instance's data is not available and - * must be fetched from the remote DWN using a `RecordsRead` message. - * - * What made this bug particularly difficult to track down is that the bug only surfaces when - * keys used to sign the record are different than the keys used to fetch the record AND both - * sets of keys are unavailable to the test Agent used by the entity that is attempting to - * fetch the record. In all of the other tests, the same test agent is used to store the keys - * for all entities (e.g., "Alice", "Bob", etc.) so the bug never surfaced. - * - * In this test, Alice is the author of the record and Bob is the recipient. Alice and Bob - * each have their own Agents, DWNs, DIDs, and keys. Alice's DWN is configured to use - * Alice's DID/keys, and Bob's DWN is configured to use Bob's DID/keys. When Alice writes a - * record to Bob's DWN, the record is signed by Alice's keys. When Bob fetches the record from - * his DWN, this test validates that the `RecordsRead` is signed by Bob's keys. - * - * SETUP STEPS: - * S1. Create a second `TestManagedAgent` that only Bob will use. - */ - const testAgentBob = await TestManagedAgent.create({ - agentClass : TestUserAgent, - agentStores : 'memory', - testDataLocation : '__TESTDATA__/AGENT_BOB' - }); - await testAgentBob.createAgentDid(); - /** - * S2. Create a "bob" Identity to author the DWN messages. - */ - ({ did: bobDid } = await testAgentBob.createIdentity({ testDwnUrls })); - await testAgentBob.agent.identityManager.import({ - did : bobDid, - identity : { name: 'Bob', did: bobDid.did }, - kms : 'local' - }); - /** - * S3. Instantiate a new `DwnApi` using Bob's test agent. - */ - dwnBob = new DwnApi({ agent: testAgentBob.agent, connectedDid: bobDid.did }); - /** - * S4. Install the email protocol to both Alice's and Bob's DWNs. - */ - let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ - message: { definition: emailProtocolDefinition } - }); - expect(aliceStatus.code).to.equal(202); - const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); - expect(alicePushStatus.code).to.equal(202); - const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ - message: { - definition: emailProtocolDefinition - } - }); - expect(bobStatus.code).to.equal(202); - const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); - expect(bobPushStatus.code).to.equal(202); - - /** - * TEST STEPS: - * - * 1. Alice creates a record but does NOT store it her local, agent-connected DWN. - */ - const { record, status } = await dwnAlice.records.write({ - data : dataTextExceedingMaxSize, - store : false, - message : { - protocol : emailProtocolDefinition.protocol, - protocolPath : 'email', - schema : emailProtocolDefinition.types.email.schema - } - }); - expect(status.code).to.equal(202); - /** - * 2. Alice writes the record to Bob's remote DWN. - */ - const { status: sendStatus } = await record!.send(bobDid.did); - expect(sendStatus.code).to.equal(202); - /** - * 3. Bob queries his remote DWN for the record that Alice just wrote. - */ - const { records: queryRecordsFrom, status: queryRecordStatusFrom } = await dwnBob.records.query({ - from : bobDid.did, - message : { filter: { recordId: record!.id }} - }); - expect(queryRecordStatusFrom.code).to.equal(200); - /** - * 4. Validate that Bob is able to access the data payload. - */ - const recordData = await queryRecordsFrom[0].data.blob(); - expect(recordData.size).to.equal(dataTextExceedingMaxSize.length); - - // Test clean-up. - await testAgentBob.clearStorage(); - }); - - it.only('returns large data payloads of records signed by another entity after remote dwn.records.query()', async () => { - /** - * WHAT IS BEING TESTED? - * - * We are testing whether a large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) record - * authored/signed by one party (Alice) can be written to another party's DWN (Bob), and that - * recipient (Bob) is able to access the data payload. This test was added to reveal a bug - * that only surfaces when accessing the data (`record.data.*`) of a record signed by a - * different entity a `Record` instance's data, which requires fetching the data from a - * remote DWN. Since the large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) data was not - * returned with the query as `encodedData`, the `Record` instance's data is not available and - * must be fetched from the remote DWN using a `RecordsRead` message. - * - * What made this bug particularly difficult to track down is that the bug only surfaces when - * keys used to sign the record are different than the keys used to fetch the record AND both - * sets of keys are unavailable to the test Agent used by the entity that is attempting to - * fetch the record. In all of the other tests, the same test agent is used to store the keys - * for all entities (e.g., "Alice", "Bob", etc.) so the bug never surfaced. - * - * In this test, Alice is the author of the record and Bob is the recipient. Alice and Bob - * each have their own Agents, DWNs, DIDs, and keys. Alice's DWN is configured to use - * Alice's DID/keys, and Bob's DWN is configured to use Bob's DID/keys. When Alice writes a - * record to Bob's DWN, the record is signed by Alice's keys. When Bob fetches the record from - * his DWN, this test validates that the `RecordsRead` is signed by Bob's keys. - * - * SETUP STEPS: - * S1. Create a second `TestManagedAgent` that only Bob will use. - */ - const testAgentBob = await TestManagedAgent.create({ - agentClass : TestUserAgent, - agentStores : 'memory', - testDataLocation : '__TESTDATA__/AGENT_BOB' - }); - await testAgentBob.createAgentDid(); - /** - * S2. Create a "bob" Identity to author the DWN messages. - */ - ({ did: bobDid } = await testAgentBob.createIdentity({ testDwnUrls })); - await testAgentBob.agent.identityManager.import({ - did : bobDid, - identity : { name: 'Bob', did: bobDid.did }, - kms : 'local' - }); - /** - * S3. Instantiate a new `DwnApi` using Bob's test agent. - */ - dwnBob = new DwnApi({ agent: testAgentBob.agent, connectedDid: bobDid.did }); - /** - * S4. Install the email protocol to both Alice's and Bob's DWNs. - */ - let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ - message: { definition: emailProtocolDefinition } - }); - expect(aliceStatus.code).to.equal(202); - const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); - expect(alicePushStatus.code).to.equal(202); - const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ - message: { - definition: emailProtocolDefinition - } - }); - expect(bobStatus.code).to.equal(202); - const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); - expect(bobPushStatus.code).to.equal(202); - - /** - * TEST STEPS: - * - * 1. Alice creates a record but does NOT store it her local, agent-connected DWN. - */ - const { record, status } = await dwnAlice.records.write({ - data : dataTextExceedingMaxSize, - store : false, - message : { - protocol : emailProtocolDefinition.protocol, - protocolPath : 'email', - schema : emailProtocolDefinition.types.email.schema - } - }); - expect(status.code).to.equal(202); - /** - * 2. Alice writes the record to Bob's remote DWN. - */ - const { status: sendStatus } = await record!.send(bobDid.did); - expect(sendStatus.code).to.equal(202); - /** - * 3. Bob queries his remote DWN for the record that Alice just wrote. - */ - const { records: queryRecordsFrom, status: queryRecordStatusFrom } = await dwnBob.records.query({ - from : bobDid.did, - message : { filter: { recordId: record!.id }} - }); - expect(queryRecordStatusFrom.code).to.equal(200); - /** - * 4. Validate that Bob is write the record to Alice's remote DWN. - */ - const { status: sendStatusToAlice } = await queryRecordsFrom[0]!.send(aliceDid.did); - expect(sendStatusToAlice.code).to.equal(202); - - // Test clean-up. - await testAgentBob.clearStorage(); - }); - - - - - - - - - - it('returns large data payloads after remote dwn.records.read()', async () => { /** Generate data that exceeds the DWN encoded data limit to ensure that the data will have to * be fetched with a RecordsRead when record.data.* is executed. */ @@ -1221,6 +994,201 @@ describe.only('Record', () => { readDataBytes = await NodeStream.consumeToBytes({ readable: readDataStream }); expect(readDataBytes.length).to.equal(inputDataBytes.length); }); + + describe('with two Agents', () => { + let testAgentBob: TestManagedAgent; + + before(async () => { + // Create a second `TestManagedAgent` that only Bob will use. + testAgentBob = await TestManagedAgent.create({ + agentClass : TestUserAgent, + agentStores : 'memory', + testDataLocation : '__TESTDATA__/AGENT_BOB' + }); + }); + + beforeEach(async () => { + await testAgentBob.clearStorage(); + + // Create an Agent DID. + await testAgent.createAgentDid(); + + // Create a new "bob" Identity to author the DWN messages. + ({ did: bobDid } = await testAgentBob.createIdentity({ testDwnUrls })); + await testAgentBob.agent.identityManager.import({ + did : bobDid, + identity : { name: 'Bob', did: bobDid.did }, + kms : 'local' + }); + + // Instantiate a new `DwnApi` using Bob's test agent. + dwnBob = new DwnApi({ agent: testAgentBob.agent, connectedDid: bobDid.did }); + }); + + after(async () => { + await testAgentBob.clearStorage(); + await testAgentBob.closeStorage(); + }); + + it('returns large data payloads of records signed by another entity after remote dwn.records.query()', async () => { + /** + * WHAT IS BEING TESTED? + * + * We are testing whether a large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) record + * authored/signed by one party (Alice) can be written to another party's DWN (Bob), and that + * recipient (Bob) is able to access the data payload. This test was added to reveal a bug + * that only surfaces when accessing the data (`record.data.*`) of a record signed by a + * different entity a `Record` instance's data, which requires fetching the data from a + * remote DWN. Since the large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) data was not + * returned with the query as `encodedData`, the `Record` instance's data is not available and + * must be fetched from the remote DWN using a `RecordsRead` message. + * + * What made this bug particularly difficult to track down is that the bug only surfaces when + * keys used to sign the record are different than the keys used to fetch the record AND both + * sets of keys are unavailable to the test Agent used by the entity that is attempting to + * fetch the record. In all of the other tests, the same test agent is used to store the keys + * for all entities (e.g., "Alice", "Bob", etc.) so the bug never surfaced. + * + * In this test, Alice is the author of the record and Bob is the recipient. Alice and Bob + * each have their own Agents, DWNs, DIDs, and keys. Alice's DWN is configured to use + * Alice's DID/keys, and Bob's DWN is configured to use Bob's DID/keys. When Alice writes a + * record to Bob's DWN, the record is signed by Alice's keys. When Bob fetches the record from + * his DWN, this test validates that the `RecordsRead` is signed by Bob's keys. + * + * SETUP STEPS: + * S1. Install the email protocol to both Alice's and Bob's DWNs. + */ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ + message: { definition: emailProtocolDefinition } + }); + expect(aliceStatus.code).to.equal(202); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); + expect(alicePushStatus.code).to.equal(202); + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ + message: { + definition: emailProtocolDefinition + } + }); + expect(bobStatus.code).to.equal(202); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); + expect(bobPushStatus.code).to.equal(202); + + /** + * TEST STEPS: + * + * 1. Alice creates a record but does NOT store it her local, agent-connected DWN. + */ + const { record, status } = await dwnAlice.records.write({ + data : dataTextExceedingMaxSize, + store : false, + message : { + protocol : emailProtocolDefinition.protocol, + protocolPath : 'email', + schema : emailProtocolDefinition.types.email.schema + } + }); + expect(status.code).to.equal(202); + /** + * 2. Alice writes the record to Bob's remote DWN. + */ + const { status: sendStatus } = await record!.send(bobDid.did); + expect(sendStatus.code).to.equal(202); + /** + * 3. Bob queries his remote DWN for the record that Alice just wrote. + */ + const { records: queryRecordsFrom, status: queryRecordStatusFrom } = await dwnBob.records.query({ + from : bobDid.did, + message : { filter: { recordId: record!.id }} + }); + expect(queryRecordStatusFrom.code).to.equal(200); + /** + * 4. Validate that Bob is able to access the data payload. + */ + const recordData = await queryRecordsFrom[0].data.blob(); + expect(recordData.size).to.equal(dataTextExceedingMaxSize.length); + }); + + it('fails to return large data payloads of records signed by another entity after remote dwn.records.query()', async () => { + /** + * ! TODO: Fix this once the bug in `dwn-sdk-js` is resolved. + * + * WHAT IS BEING TESTED? + * + * We are testing whether a large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) record + * authored/signed by one party (Alice) can be written to another party's DWN (Bob), and that + * recipient (Bob) is able to access the data payload. This test was added to reveal a bug + * that only surfaces when accessing the data (`record.data.*`) of a record signed by a + * different entity a `Record` instance's data, which requires fetching the data from a + * remote DWN. Since the large (> `DwnConstant.maxDataSizeAllowedToBeEncoded`) data was not + * returned with the query as `encodedData`, the `Record` instance's data is not available and + * must be fetched from the remote DWN using a `RecordsRead` message. + * + * What made this bug particularly difficult to track down is that the bug only surfaces when + * keys used to sign the record are different than the keys used to fetch the record AND both + * sets of keys are unavailable to the test Agent used by the entity that is attempting to + * fetch the record. In all of the other tests, the same test agent is used to store the keys + * for all entities (e.g., "Alice", "Bob", etc.) so the bug never surfaced. + * + * In this test, Alice is the author of the record and Bob is the recipient. Alice and Bob + * each have their own Agents, DWNs, DIDs, and keys. Alice's DWN is configured to use + * Alice's DID/keys, and Bob's DWN is configured to use Bob's DID/keys. When Alice writes a + * record to Bob's DWN, the record is signed by Alice's keys. When Bob fetches the record from + * his DWN, this test validates that the `RecordsRead` is signed by Bob's keys. + * + * SETUP STEPS: + * S1. Install the email protocol to both Alice's and Bob's DWNs. + */ + let { protocol: aliceProtocol, status: aliceStatus } = await dwnAlice.protocols.configure({ + message: { definition: emailProtocolDefinition } + }); + expect(aliceStatus.code).to.equal(202); + const { status: alicePushStatus } = await aliceProtocol!.send(aliceDid.did); + expect(alicePushStatus.code).to.equal(202); + const { protocol: bobProtocol, status: bobStatus } = await dwnBob.protocols.configure({ + message: { + definition: emailProtocolDefinition + } + }); + expect(bobStatus.code).to.equal(202); + const { status: bobPushStatus } = await bobProtocol!.send(bobDid.did); + expect(bobPushStatus.code).to.equal(202); + + /** + * TEST STEPS: + * + * 1. Alice creates a record but does NOT store it her local, agent-connected DWN. + */ + const { record, status } = await dwnAlice.records.write({ + data : dataTextExceedingMaxSize, + store : false, + message : { + protocol : emailProtocolDefinition.protocol, + protocolPath : 'email', + schema : emailProtocolDefinition.types.email.schema + } + }); + expect(status.code).to.equal(202); + /** + * 2. Alice writes the record to Bob's remote DWN. + */ + const { status: sendStatus } = await record!.send(bobDid.did); + expect(sendStatus.code).to.equal(202); + /** + * 3. Bob queries his remote DWN for the record that Alice just wrote. + */ + const { records: queryRecordsFrom, status: queryRecordStatusFrom } = await dwnBob.records.query({ + from : bobDid.did, + message : { filter: { recordId: record!.id }} + }); + expect(queryRecordStatusFrom.code).to.equal(200); + /** + * 4. Validate that Bob is able to write the record to Alice's remote DWN. + */ + const { status: sendStatusToAlice } = await queryRecordsFrom[0]!.send(aliceDid.did); + expect(sendStatusToAlice.code).to.equal(401); + expect(sendStatusToAlice.detail).to.equal(`Cannot read properties of undefined (reading 'authorization')`); + }); + }); }); describe('send()', () => { @@ -1806,7 +1774,6 @@ describe.only('Record', () => { it('should return all defined properties', async () => { // RecordOptions properties const author = aliceDid.did; - const target = aliceDid.did; // Retrieve `#dwn` service entry. const [ didDwnService ] = didUtils.getServices({ didDocument: aliceDid.document, id: '#dwn' }); @@ -1890,9 +1857,9 @@ describe.only('Record', () => { // Create record using test RecordsWriteMessage. const record = new Record(testAgent.agent, { ...recordsWrite.message, - encodedData: dataBlob, - target, - author, + encodedData : dataBlob, + author : aliceDid.did, + connectedDid : aliceDid.did, }); // Call toJSON() method. @@ -1900,7 +1867,6 @@ describe.only('Record', () => { // Retained Record properties. expect(recordJson.author).to.equal(author); - expect(recordJson.target).to.equal(target); // Retained RecordsWriteMessage top-level properties. expect(record.contextId).to.equal(recordsWrite.message.contextId);