diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 37988ca70..bcc481358 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -830,6 +830,7 @@ export class DwnApi { connectedDid : this.connectedDid, delegateDid : this.delegateDid, permissionsApi : this.permissionsApi, + protocolRole : request.message.protocolRole, request }) }; diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index fd0043b8c..abb479608 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -9,6 +9,7 @@ import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './utils/test-config.js'; import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' assert { type: 'json' }; import photosProtocolDefinition from './fixtures/protocol-definitions/photos.json' assert { type: 'json' }; +import notesProtocolDefinition from './fixtures/protocol-definitions/notes.json' assert { type: 'json' }; import { DwnConstant, DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Poller, Time } from '@tbd54566975/dwn-sdk-js'; import { PermissionGrant } from '../src/permission-grant.js'; import { Record } from '../src/record.js'; @@ -1055,123 +1056,6 @@ describe('DwnApi', () => { expect(await result.record?.data.json()).to.deep.equal(dataJson); }); - it('ensure that a protocolRole used to query is also used to read the data of the result', async () => { - // Configure the photos protocol on Alice and Bob's local and remote DWNs. - const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ - message: { - definition: photosProtocolDefinition - } - }); - expect(bobProtocolStatus.code).to.equal(202); - const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); - expect(bobRemoteProtocolStatus.code).to.equal(202); - - // Bob creates an album - const { status: albumCreateStatus, record: albumRecord } = await dwnBob.records.create({ - data : 'My Album', - message : { - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album', - schema : photosProtocolDefinition.types.album.schema, - dataFormat : 'text/plain' - } - }); - expect(albumCreateStatus.code).to.equal(202); - const { status: albumSendStatus } = await albumRecord.send(); - expect(albumSendStatus.code).to.equal(202); - - // Bob makes Alice a `participant` and sends the record to her and his own remote node. - const { status: participantCreateStatus, record: participantRecord} = await dwnBob.records.create({ - data : 'test', - message : { - parentContextId : albumRecord.contextId, - recipient : aliceDid.uri, - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/participant', - schema : photosProtocolDefinition.types.participant.schema, - dataFormat : 'text/plain' - } - }); - expect(participantCreateStatus.code).to.equal(202); - const { status: bobParticipantSendStatus } = await participantRecord.send(bobDid.uri); - expect(bobParticipantSendStatus.code).to.equal(202); - - // bob adds 3 photos to the album - for (let i = 0; i < 3; i++) { - const { status: photoCreateStatus, record: photoRecord } = await dwnBob.records.create({ - data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), - message : { - parentContextId : albumRecord.contextId, - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - schema : photosProtocolDefinition.types.photo.schema, - dataFormat : 'text/plain', - } - }); - expect(photoCreateStatus.code).to.equal(202); - const { status: photoSendStatus } = await photoRecord.send(); - expect(photoSendStatus.code).to.equal(202); - } - - // alice uses the role to add a photo to the album - const { status: photoCreateStatusAlice, record: photoRecordAlice } = await dwnAlice.records.create({ - store : false, - data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), - message : { - parentContextId : albumRecord.contextId, - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - protocolRole : 'album/participant', - schema : photosProtocolDefinition.types.photo.schema, - dataFormat : 'text/plain' - } - }); - expect(photoCreateStatusAlice.code).to.equal(202); - const { status: albumSendStatusAlice } = await photoRecordAlice.send(bobDid.uri); - expect(albumSendStatusAlice.code).to.equal(202); - - //SANITY: Alice attempts to fetch the photos without the role, she should only see her own photo - const { status: alicePhotosReadResultWithoutRole, records: alicePhotosRecordsWithoutRole } = await dwnAlice.records.query({ - from : bobDid.uri, - message : { - filter: { - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - contextId : albumRecord.contextId - } - } - }); - expect(alicePhotosReadResultWithoutRole.code).to.equal(200); - expect(alicePhotosRecordsWithoutRole).to.exist; - expect(alicePhotosRecordsWithoutRole).to.have.lengthOf(1); - - // Attempt to read the data of the photo, which should succeed - const readResultWithoutRole = await alicePhotosRecordsWithoutRole[0].data.text(); - expect(readResultWithoutRole.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); - - // Alice fetches all of the photos from the album - const alicePhotosReadResult = await dwnAlice.records.query({ - from : bobDid.uri, - message : { - protocolRole : 'album/participant', - filter : { - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - contextId : albumRecord.contextId - } - } - }); - expect(alicePhotosReadResult.status.code).to.equal(200); - expect(alicePhotosReadResult.records).to.exist; - expect(alicePhotosReadResult.records).to.have.lengthOf(4); - - // attempt to read data from the photos - for (const record of alicePhotosReadResult.records) { - const readResult = await record.data.text(); - expect(readResult.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); - } - }); - it('creates a role record for another user that they can use to create role-based records', async () => { /** * WHAT IS BEING TESTED? @@ -2196,6 +2080,75 @@ describe('DwnApi', () => { expect(fooBarResult.records![0].id).to.equal(record.id); expect(fooBarResult.records![0].tags).to.deep.equal({ foo: 'bar' }); }); + + it('ensures that a protocolRole used to query is also used to read the data of the resulted records', async () => { + // Bob configures the notes protocol for himself + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: notesProtocolDefinition + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); + expect(bobRemoteProtocolStatus.code).to.equal(202); + + // Bob creates a few notes ensuring that the data is larger than the max encoded size + // that way the data will be requested with a separate `read` request + const recordData: Map = new Map(); + for (let i = 0; i < 3; i++) { + const data = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ + data, + message: { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note', + schema : notesProtocolDefinition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(noteCreateStatus.code).to.equal(202); + const { status: noteSendStatus } = await noteRecord.send(); + expect(noteSendStatus.code).to.equal(202); + recordData.set(noteRecord.id, data); + } + + // Bob makes Alice a `friend` to allow her to read and comment on his notes + const { status: friendCreateStatus, record: friendRecord} = await dwnBob.records.create({ + data : 'friend!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocolDefinition.protocol, + protocolPath : 'friend', + schema : notesProtocolDefinition.types.friend.schema, + dataFormat : 'text/plain' + } + }); + expect(friendCreateStatus.code).to.equal(202); + const { status: bobFriendSendStatus } = await friendRecord.send(bobDid.uri); + expect(bobFriendSendStatus.code).to.equal(202); + + // alice uses the role to query for the available notes + const { status: notesQueryStatus, records: noteRecords } = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + protocolRole : 'friend', + filter : { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note' + } + } + }); + expect(notesQueryStatus.code).to.equal(200); + expect(noteRecords).to.exist; + expect(noteRecords).to.have.lengthOf(3); + + // Alice attempts to read the data of the notes, which should succeed + for (const record of noteRecords) { + const readResult = await record.data.text(); + const expectedData = recordData.get(record.id); + expect(readResult).to.equal(expectedData); + } + }); }); }); @@ -2562,6 +2515,86 @@ describe('DwnApi', () => { expect(record.deleted).to.be.false; }); }); + + it('ensures that a protocolRole used to subscribe is also used to read the data of the resulted records', async () => { + // Bob configures the notes protocol for himself + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: notesProtocolDefinition + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); + expect(bobRemoteProtocolStatus.code).to.equal(202); + + + // Bob makes Alice a `friend` to allow her to read and comment on his notes + const { status: friendCreateStatus, record: friendRecord} = await dwnBob.records.create({ + data : 'friend!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocolDefinition.protocol, + protocolPath : 'friend', + schema : notesProtocolDefinition.types.friend.schema, + dataFormat : 'text/plain' + } + }); + expect(friendCreateStatus.code).to.equal(202); + const { status: bobFriendSendStatus } = await friendRecord.send(bobDid.uri); + expect(bobFriendSendStatus.code).to.equal(202); + + // Alice subscribes to the notes protocol using the role + const notes: Map = new Map(); + const subscriptionHandler = async (record: Record) => { + notes.set(record.id, record); + }; + + // alice uses the role to query for the available notes + const { status: notesSubscribeStatus, subscription } = await dwnAlice.records.subscribe({ + from : bobDid.uri, + message : { + protocolRole : 'friend', + filter : { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note' + } + }, + subscriptionHandler + }); + expect(notesSubscribeStatus.code).to.equal(200); + expect(subscription).to.exist; + + // Bob creates a few notes ensuring that the data is larger than the max encoded size + // that way the data will be requested with a separate `read` request + const recordData: Map = new Map(); + for (let i = 0; i < 3; i++) { + const data = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ + data, + message: { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note', + schema : notesProtocolDefinition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(noteCreateStatus.code).to.equal(202); + const { status: noteSendStatus } = await noteRecord.send(); + expect(noteSendStatus.code).to.equal(202); + recordData.set(noteRecord.id, data); + } + + // poll for the note records to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(notes.size).to.equal(3); + }); + + for (const record of notes.values()) { + const readResult = await record.data.text(); + const expectedData = recordData.get(record.id); + expect(readResult).to.equal(expectedData); + } + }); }); }); diff --git a/packages/api/tests/fixtures/protocol-definitions/notes.json b/packages/api/tests/fixtures/protocol-definitions/notes.json new file mode 100644 index 000000000..a98acf917 --- /dev/null +++ b/packages/api/tests/fixtures/protocol-definitions/notes.json @@ -0,0 +1,48 @@ +{ + "protocol": "http://notes-protocol.xyz", + "published": true, + "types": { + "note": { + "schema": "http://notes-protocol.xyz/schema/note", + "dataFormats": [ + "text/plain", + "application/json" + ] + }, + "comment": { + "schema": "http://notes-protocol.xyz/schema/comment", + "dataFormats": [ + "text/plain", + "application/json" + ] + }, + "friend" : { + "schema": "http://notes-protocol.xyz/schema/friend", + "dataFormats": [ + "text/plain", + "application/json" + ] + } + }, + "structure": { + "friend" :{ + "$role": true + }, + "note": { + "$actions": [ + { + "role": "friend", + "can": ["read", "query", "subscribe"] + } + ], + "comment": { + "$actions": [ + { + "role": "friend", + "can": ["create", "delete", "read", "query", "subscribe"] + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/api/tests/fixtures/protocol-definitions/photos.json b/packages/api/tests/fixtures/protocol-definitions/photos.json index dfa3083dd..4a5c6c4ca 100644 --- a/packages/api/tests/fixtures/protocol-definitions/photos.json +++ b/packages/api/tests/fixtures/protocol-definitions/photos.json @@ -32,7 +32,7 @@ { "role": "friend", "can": [ - "create", "update", "read", "query", "subscribe" + "create", "update" ] } ], @@ -54,7 +54,7 @@ { "role": "album/participant", "can": [ - "create", "update", "read", "query", "subscribe" + "create", "update" ] } ] @@ -64,7 +64,7 @@ { "role": "album/participant", "can": [ - "create", "update", "read", "query", "subscribe" + "create", "update" ] }, {