Skip to content

Commit

Permalink
Fix bug with record.data.* after record queries (#298)
Browse files Browse the repository at this point in the history
* Improve Type safety now that RecordsReadReply is part of DwnResponse
* Improve test coverage by ensuring that the DWN query record size limit is imported from DWN SDK
* Fix bug caused by RecordsRead now taking a filter property and add tests
  • Loading branch information
frankhinek authored Nov 19, 2023
1 parent fb61e3b commit 7efd9bf
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 26 deletions.
5 changes: 2 additions & 3 deletions packages/agent/tests/dwn-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
EventsGetReply,
EventsGetMessage,
MessagesGetReply,
RecordsReadReply,
RecordsQueryReply,
UnionMessageReply,
MessagesGetMessage,
Expand Down Expand Up @@ -377,7 +376,7 @@ describe('DwnManager', () => {
expect(readMessage).to.have.property('authorization');
expect(readMessage).to.have.property('descriptor');

const readReply = readResponse.reply as RecordsReadReply;
const readReply = readResponse.reply;
expect(readReply).to.have.property('status');
expect(readReply.status.code).to.equal(200);
expect(readReply).to.have.property('record');
Expand Down Expand Up @@ -552,7 +551,7 @@ describe('DwnManager', () => {
expect(readMessage.descriptor.method).to.equal('Read');
expect(readMessage.descriptor.interface).to.equal('Records');

const readReply = response.reply as RecordsReadReply;
const readReply = response.reply;
expect(readReply.record).to.exist;

const record = readReply.record as unknown as RecordsWriteMessage & { data: ReadableStream };
Expand Down
12 changes: 5 additions & 7 deletions packages/api/src/dwn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,13 +440,11 @@ export class DwnApi {
/**
* Writes a record to the DWN
*
* As a convenience, the Record instance returned will cache a copy of the data if the
* data size, in bytes, is less than the DWN 'max data size allowed to be encoded'
* parameter of 10KB. This is done to maintain consistency with other DWN methods,
* like RecordsQuery, that include relatively small data payloads when returning
* RecordsWrite message properties. Regardless of data size, methods such as
* `record.data.stream()` will return the data when called even if it requires fetching
* from the DWN datastore.
* As a convenience, the Record instance returned will cache a copy of the data. This is done
* to maintain consistency with other DWN methods, like RecordsQuery, that include relatively
* small data payloads when returning RecordsWrite message properties. Regardless of data
* size, methods such as `record.data.stream()` will return the data when called even if it
* requires fetching from the DWN datastore.
*/
write: async (request: RecordsWriteRequest): Promise<RecordsWriteResponse> => {
const messageOptions: Partial<RecordsWriteOptions> = {
Expand Down
7 changes: 3 additions & 4 deletions packages/api/src/record.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Web5Agent } from '@web5/agent';
import type { Readable } from 'readable-stream';
import type {
RecordsReadReply,
RecordsWriteMessage,
RecordsWriteOptions,
RecordsWriteDescriptor,
Expand Down Expand Up @@ -191,12 +190,12 @@ export class Record implements RecordModel {
// If neither of the above are true, then the record must be fetched from the DWN.
this._readableStream = this._agent.processDwnRequest({
author : this.author,
messageOptions : { recordId: this.id },
messageOptions : { filter: { recordId: this.id } },
messageType : DwnInterfaceName.Records + DwnMethodName.Read,
target : this.target,
})
.then(response => response.reply as RecordsReadReply)
.then(reply => reply.record.data as Readable)
.then(response => response.reply)
.then(reply => reply.record.data)
.catch(error => { throw new Error(`Error encountered while attempting to read data: ${error.message}`); });
}

Expand Down
53 changes: 53 additions & 0 deletions packages/api/tests/dwn-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,59 @@ describe('DwnApi', () => {
expect(deleteResult.status.code).to.equal(401);
expect(deleteResult.status.detail).to.include('message failed authorization');
});

it('deletes records that were authored/signed by another DID', async () => {
/**
* WHAT IS BEING TESTED?
*
* We are testing whether a record authored/signed by one party (Alice) can be written to
* another party's DWN (Bob), and that recipient (Bob) is able to delete the record.
*
* TEST SETUP STEPS:
* 1. Configure the email protocol on Bob's local DWN.
*/
const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({
message: {
definition: emailProtocolDefinition
}
});
expect(bobProtocolStatus.code).to.equal(202);
/**
* 2. Configure the email protocol on Bob's remote DWN.
*/
const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.did);
expect(bobRemoteProtocolStatus.code).to.equal(202);
/**
* 3. Alice creates a record, but doesn't store it locally.
*/
const { status: createStatus, record: testRecord} = await dwnAlice.records.create({
store : false,
data : 'test',
message : {
protocol : 'http://email-protocol.xyz',
protocolPath : 'email',
schema : 'http://email-protocol.xyz/schema/email',
dataFormat : 'text/plain'
}
});
expect(createStatus.code).to.equal(202);
expect(testRecord.author).to.equal(aliceDid.did);
/**
* 4. Alice writes the record to Bob's remote DWN.
*/
const { status: sendStatus } = await testRecord.send(bobDid.did);
expect(sendStatus.code).to.equal(202);
/**
* 5. Bob deletes the record from his remote DWN.
*/
const deleteResult = await dwnBob.records.delete({
from : bobDid.did,
message : {
recordId: testRecord.id
}
});
expect(deleteResult.status.code).to.equal(202);
});
});
});

Expand Down
101 changes: 89 additions & 12 deletions packages/api/tests/record.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import chaiAsPromised from 'chai-as-promised';
import { utils as didUtils } from '@web5/dids';
import { TestManagedAgent } from '@web5/agent';
import {
DwnConstant,
RecordsWrite,
DwnMethodName,
DwnInterfaceName,
Expand Down Expand Up @@ -254,10 +255,10 @@ describe('Record', () => {
it('returns large data payloads after dwn.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.blob() is executed.
const dataJson = TestDataGenerator.randomJson(11_000);
const dataJson = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);
const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson));

// Write the 11KB record to agent-connected DWN.
// Write the large record to agent-connected DWN.
const { record, status } = await dwn.records.write({ data: dataJson });

expect(status.code).to.equal(202);
Expand All @@ -271,13 +272,39 @@ describe('Record', () => {
expect(readDataBytes).to.deep.equal(inputDataBytes);
});

it('returns large data payloads after dwn.records.query()', 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.blob() is executed. */
const dataJson = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);
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 });
expect(status.code).to.equal(202);

// Query for the record that was just created.
const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({
message: { filter: { recordId: record!.id }}
});
expect(queryRecordStatus.code).to.equal(200);

// Confirm that the size, in bytes, of the data read as a Blob matches the original input data.
const [ queryRecord ] = queryRecords;
const queriedDataBlob = await queryRecord.data.blob();
expect(queriedDataBlob.size).to.equal(inputDataBytes.length);

// Convert the Blob into an array and ensure it matches the input data, byte for byte.
const queriedDataBytes = new Uint8Array(await queriedDataBlob.arrayBuffer());
expect(queriedDataBytes).to.deep.equal(inputDataBytes);
});

it('returns large data payloads after 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.blob() is executed.
const dataJson = TestDataGenerator.randomJson(11_000);
const dataJson = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);
const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson));

// Write the 11KB record to agent-connected DWN.
// Write the large record to agent-connected DWN.
const { record, status } = await dwn.records.write({ data: dataJson });

expect(status.code).to.equal(202);
Expand Down Expand Up @@ -346,10 +373,10 @@ describe('Record', () => {
it('returns large data payloads after dwn.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.json() is executed.
const dataJson = TestDataGenerator.randomJson(11_000);
const dataJson = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);
const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson));

// Write the 11KB record to agent-connected DWN.
// Write the large record to agent-connected DWN.
const { record, status } = await dwn.records.write({ data: dataJson });

expect(status.code).to.equal(202);
Expand All @@ -363,13 +390,39 @@ describe('Record', () => {
expect(readDataBytes).to.deep.equal(inputDataBytes);
});

it('returns large data payloads after dwn.records.query()', 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.json() is executed. */
const dataJson = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);
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 });
expect(status.code).to.equal(202);

// Query for the record that was just created.
const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({
message: { filter: { recordId: record!.id }}
});
expect(queryRecordStatus.code).to.equal(200);

// Confirm that the size, in bytes, of the data read as JSON matches the original input data.
const [ queryRecord ] = queryRecords;
const queriedDataBlob = await queryRecord!.data.json();

// Convert the JSON to bytes and ensure it matches the input data, byte for byte.
const queriedDataBytes = new TextEncoder().encode(JSON.stringify(queriedDataBlob));
expect(queriedDataBytes.length).to.equal(inputDataBytes.length);
expect(queriedDataBytes).to.deep.equal(inputDataBytes);
});

it('returns large data payloads after 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.json() is executed.
const dataJson = TestDataGenerator.randomJson(11_000);
const dataJson = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);
const inputDataBytes = new TextEncoder().encode(JSON.stringify(dataJson));

// Write the 11KB record to agent-connected DWN.
// Write the large record to agent-connected DWN.
const { record, status } = await dwn.records.write({ data: dataJson });

expect(status.code).to.equal(202);
Expand Down Expand Up @@ -434,9 +487,9 @@ describe('Record', () => {
it('returns large data payloads after dwn.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(11_000);
const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);

// Write the 11KB record to agent-connected DWN.
// Write the large record to agent-connected DWN.
const { record, status } = await dwn.records.write({ data: dataText });

expect(status.code).to.equal(202);
Expand All @@ -449,12 +502,36 @@ describe('Record', () => {
expect(readDataText).to.deep.equal(dataText);
});

it('returns large data payloads after dwn.records.query()', 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.blob() 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 });
expect(status.code).to.equal(202);

// Query for the record that was just created.
const { records: queryRecords, status: queryRecordStatus } = await dwn.records.query({
message: { filter: { recordId: record!.id }}
});
expect(queryRecordStatus.code).to.equal(200);

// Confirm that the length of the data read as text matches the original input data.
const [ queryRecord ] = queryRecords;
const queriedDataText = await queryRecord!.data.text();
expect(queriedDataText.length).to.equal(dataText.length);

// Ensure the text returned matches the input data, char for char.
expect(queriedDataText).to.deep.equal(dataText);
});

it('returns large data payloads after 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.text() is executed.
const dataText = TestDataGenerator.randomString(11_000);
const dataText = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000);

// Write the 11KB record to agent-connected DWN.
// Write the large record to agent-connected DWN.
const { record, status } = await dwn.records.write({ data: dataText });

expect(status.code).to.equal(202);
Expand Down

0 comments on commit 7efd9bf

Please sign in to comment.