From 9f257591fa6b50176877bb1466c7f96e8a3c4efe Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Tue, 20 Aug 2024 11:01:12 -0400 Subject: [PATCH] RecordsQuery & RecordsSubscribe supports an array for `author` and `recipient` (#777) Introduce filtering by multiple authors or recipients. The filters are backward compatible, so that `author` and `recipient` filters now accept a `string` or `Array`. NOTE: If an empty array is passed into the filter, it is treated as an undefined filter. --- .../interface-methods/records-filter.json | 18 +- src/handlers/records-query.ts | 11 +- src/handlers/records-subscribe.ts | 11 +- src/types/records-types.ts | 4 +- src/utils/records.ts | 40 + tests/handlers/records-query.spec.ts | 724 ++++++++++++++--- tests/scenarios/aggregator.spec.ts | 747 ++++++++++++++++++ tests/scenarios/subscriptions.spec.ts | 267 ++++++- 8 files changed, 1689 insertions(+), 133 deletions(-) create mode 100644 tests/scenarios/aggregator.spec.ts diff --git a/json-schemas/interface-methods/records-filter.json b/json-schemas/interface-methods/records-filter.json index 2c1107cfa..0e9d0e1d4 100644 --- a/json-schemas/interface-methods/records-filter.json +++ b/json-schemas/interface-methods/records-filter.json @@ -12,13 +12,27 @@ "type": "string" }, "author": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" + "oneOf": [{ + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" + },{ + "type": "array", + "items": { + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" + } + }] }, "attester": { "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" }, "recipient": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" + "oneOf": [{ + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" + },{ + "type": "array", + "items": { + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" + } + }] }, "contextId": { "type": "string" diff --git a/src/handlers/records-query.ts b/src/handlers/records-query.ts index 9bd269648..b90754a65 100644 --- a/src/handlers/records-query.ts +++ b/src/handlers/records-query.ts @@ -151,16 +151,17 @@ export class RecordsQueryHandler implements MethodHandler { } if (Records.filterIncludesUnpublishedRecords(filter)) { - filters.push(RecordsQueryHandler.buildUnpublishedRecordsByQueryAuthorFilter(recordsQuery)); - - const recipientFilter = recordsQuery.message.descriptor.filter.recipient; - if (recipientFilter === undefined || recipientFilter === recordsQuery.author) { - filters.push(RecordsQueryHandler.buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery)); + if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsQuery.author!)) { + filters.push(RecordsQueryHandler.buildUnpublishedRecordsByQueryAuthorFilter(recordsQuery)); } if (Records.shouldProtocolAuthorize(recordsQuery.signaturePayload!)) { filters.push(RecordsQueryHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsQuery)); } + + if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsQuery.author!)) { + filters.push(RecordsQueryHandler.buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery)); + } } const messageSort = this.convertDateSort(dateSort); diff --git a/src/handlers/records-subscribe.ts b/src/handlers/records-subscribe.ts index ea1416ae5..850e7a6c1 100644 --- a/src/handlers/records-subscribe.ts +++ b/src/handlers/records-subscribe.ts @@ -127,16 +127,17 @@ export class RecordsSubscribeHandler implements MethodHandler { } if (Records.filterIncludesUnpublishedRecords(filter)) { - filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsBySubscribeAuthorFilter(recordsSubscribe)); - - const recipientFilter = recordsSubscribe.message.descriptor.filter.recipient; - if (recipientFilter === undefined || recipientFilter === recordsSubscribe.author) { - filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsForSubscribeAuthorFilter(recordsSubscribe)); + if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsSubscribe.author!)) { + filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsBySubscribeAuthorFilter(recordsSubscribe)); } if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload!)) { filters.push(RecordsSubscribeHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsSubscribe)); } + + if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author!)) { + filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsForSubscribeAuthorFilter(recordsSubscribe)); + } } return filters; } diff --git a/src/types/records-types.ts b/src/types/records-types.ts index 4d94d0fd7..a2abe2934 100644 --- a/src/types/records-types.ts +++ b/src/types/records-types.ts @@ -135,9 +135,9 @@ export type RecordsFilter = { /** * The logical author of the record */ - author?: string; + author?: string | string[]; attester?: string; - recipient?: string; + recipient?: string | string[]; protocol?: string; protocolPath?: string; published?: boolean; diff --git a/src/utils/records.ts b/src/utils/records.ts index ff30543b0..e6f33d643 100644 --- a/src/utils/records.ts +++ b/src/utils/records.ts @@ -382,6 +382,16 @@ export class Records { filterCopy.contextId = contextIdPrefixFilter; } + // if the author filter is an array and it's empty, we should remove it from the filter as it will always return no results. + if (Array.isArray(filterCopy.author) && filterCopy.author.length === 0) { + delete filterCopy.author; + } + + // if the recipient filter is an array and it's empty, we should remove it from the filter as it will always return no results. + if (Array.isArray(filterCopy.recipient) && filterCopy.recipient.length === 0) { + delete filterCopy.recipient; + } + return filterCopy as Filter; } @@ -531,4 +541,34 @@ export class Records { return true; } + + /** + * Checks whether or not the incoming records query filter should build an unpublished recipient MessageStore filter. + * + * @param filter The incoming RecordsFilter to evaluate against. + * @param recipient The recipient to check against the filter, typically the query/subscribe message author. + * @returns {boolean} True if the filter contains the recipient, or if the recipient filter is undefined/empty. + */ + static shouldBuildUnpublishedRecipientFilter(filter: RecordsFilter, recipient: string): boolean { + const { recipient: recipientFilter } = filter; + + return Array.isArray(recipientFilter) ? + recipientFilter.length === 0 || recipientFilter.includes(recipient) : + recipientFilter === undefined || recipientFilter === recipient; + } + + /** + * Checks whether or not the incoming records query filter should build an unpublished author MessageStore filter. + * + * @param filter The incoming RecordsFilter to evaluate against. + * @param author The author to check against the filter, typically the query/subscribe message author. + * @returns {boolean} True if the filter contains the author, or if the author filter is undefined/empty. + */ + static shouldBuildUnpublishedAuthorFilter(filter: RecordsFilter, author: string): boolean { + const { author: authorFilter } = filter; + + return Array.isArray(authorFilter) ? + authorFilter.length === 0 || authorFilter.includes(author) : + authorFilter === undefined || authorFilter === author; + } } diff --git a/tests/handlers/records-query.spec.ts b/tests/handlers/records-query.spec.ts index d528114bf..a7bd14bfa 100644 --- a/tests/handlers/records-query.spec.ts +++ b/tests/handlers/records-query.spec.ts @@ -316,6 +316,151 @@ export function testRecordsQueryHandler(): void { expect(queryReply.status.code).to.equal(200); expect(queryReply.entries?.length).to.equal(1); expect(queryReply.entries![0].recordId).to.equal(bobAuthorWrite.message.recordId); + + // empty array for author should return all same as undefined author field + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + author : [], + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(2); + + // query for both authors explicitly + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + author : [alice.did, bob.did], + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(2); + }); + + it('should be able to query by recipient', async () => { + // scenario alice authors records for bob and carol into alice's DWN. + // bob and carol are able to filter for records for them. + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + const carol = await TestDataGenerator.generateDidKeyPersona(); + + const protocolDefinition = freeForAll; + + const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ + author: alice, + protocolDefinition + }); + const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); + expect(protocolsConfigureReply.status.code).to.equal(202); + + const aliceToBob = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const aliceToBobReply = await dwn.processMessage(alice.did, aliceToBob.message, { dataStream: aliceToBob.dataStream }); + expect(aliceToBobReply.status.code).to.equal(202); + + const aliceToCarol = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : carol.did, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const aliceToCarolReply = await dwn.processMessage(alice.did, aliceToCarol.message, { dataStream: aliceToCarol.dataStream }); + expect(aliceToCarolReply.status.code).to.equal(202); + + // alice queries with an empty filter, gets both + let recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + let queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(2); + + // filter for bob as recipient + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + recipient : bob.did, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(1); + expect(queryReply.entries![0].recordId).to.equal(aliceToBob.message.recordId); + + // filter for carol as recipient + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + recipient : carol.did, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(1); + expect(queryReply.entries![0].recordId).to.equal(aliceToCarol.message.recordId); + + // empty array for recipient should return all same as undefined recipient field + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + recipient : [], + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(2); + + // query for both recipients explicitly + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + recipient : [bob.did, carol.did], + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + } + }); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(2); }); it('should be able to query for published records', async () => { @@ -1466,121 +1611,524 @@ export function testRecordsQueryHandler(): void { expect((publishedReply.entries![0].descriptor as RecordsWriteDescriptor).schema).to.equal('https://schema2'); }); - it('should only return published records and unpublished records that is meant for author', async () => { - // write 4 records into Alice's DB: - // 1st is unpublished authored by Alice - // 2nd is also unpublished authored by Alice, but is meant for (has recipient as) Bob - // 3rd is also unpublished but is authored by Bob - // 4th is published - // 5th is published, authored by Alice and is meant for Carol as recipient; + it('should only return published records and unpublished records that are meant for specific recipient(s)', async () => { + // scenario: Alice installs a free-for-all protocol on her DWN + // She writes both private and public messages for bob and carol, carol and bob also write public and privet messages for alice and each other + // Bob, Alice and Carol should only be able to see private messages pertaining to themselves, and any public messages filtered by a recipient + // Bob, Alice and Carol should be able to filter for ONLY public messages or ONLY private messages const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); const carol = await TestDataGenerator.generateDidKeyPersona(); - const schema = 'schema1'; - const record1Data = await TestDataGenerator.generateRecordsWrite( - { author: alice, schema, data: Encoder.stringToBytes('1') } - ); - const record2Data = await TestDataGenerator.generateRecordsWrite( - { author: alice, schema, protocol: 'protocol', protocolPath: 'path', recipient: bob.did, data: Encoder.stringToBytes('2') } - ); - const record3Data = await TestDataGenerator.generateRecordsWrite( - { author: bob, schema, protocol: 'protocol', protocolPath: 'path', recipient: alice.did, data: Encoder.stringToBytes('3') } - ); - const record4Data = await TestDataGenerator.generateRecordsWrite( - { author: alice, schema, data: Encoder.stringToBytes('4'), published: true } - ); - const record5Data = await TestDataGenerator.generateRecordsWrite( - { author: alice, schema, data: Encoder.stringToBytes('5'), published: true, recipient: carol.did } - ); + // install the free-for-all protocol on Alice's DWN + const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : freeForAll + }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); + expect(protocolConfigureReply.status.code).to.equal(202); - // directly inserting data to datastore so that we don't have to setup to grant Bob permission to write to Alice's DWN - const recordsWriteHandler = new RecordsWriteHandler(didResolver, messageStore, dataStore, eventLog, eventStream); + // write private records for bob and carol + const alicePrivateToBob = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); - const additionalIndexes1 = await record1Data.recordsWrite.constructIndexes(true); - record1Data.message = await recordsWriteHandler.cloneAndAddEncodedData(record1Data.message, record1Data.dataBytes!); - await messageStore.put(alice.did, record1Data.message, additionalIndexes1); - await eventLog.append(alice.did, await Message.getCid(record1Data.message), additionalIndexes1); + const alicePrivateToBobReply = await dwn.processMessage(alice.did, alicePrivateToBob.message, { dataStream: alicePrivateToBob.dataStream }); + expect(alicePrivateToBobReply.status.code).to.equal(202); - const additionalIndexes2 = await record2Data.recordsWrite.constructIndexes(true); - record2Data.message = await recordsWriteHandler.cloneAndAddEncodedData(record2Data.message,record2Data.dataBytes!); - await messageStore.put(alice.did, record2Data.message, additionalIndexes2); - await eventLog.append(alice.did, await Message.getCid(record2Data.message), additionalIndexes1); + const alicePrivateToCarol = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const alicePrivateToCarolReply = await dwn.processMessage(alice.did, alicePrivateToCarol.message, { + dataStream: alicePrivateToCarol.dataStream + }); + expect(alicePrivateToCarolReply.status.code).to.equal(202); + + // write private records from carol to alice and bob + const carolPrivateToAlice = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const carolPrivateToAliceReply = await dwn.processMessage(alice.did, carolPrivateToAlice.message, { + dataStream: carolPrivateToAlice.dataStream + }); + expect(carolPrivateToAliceReply.status.code).to.equal(202); + + const carolPrivateToBob = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const carolPrivateToBobReply = await dwn.processMessage(alice.did, carolPrivateToBob.message, { + dataStream: carolPrivateToBob.dataStream + }); + expect(carolPrivateToBobReply.status.code).to.equal(202); + + // write private records from bob to alice and carol + const bobPrivateToAlice = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); - const additionalIndexes3 = await record3Data.recordsWrite.constructIndexes(true); - record3Data.message = await recordsWriteHandler.cloneAndAddEncodedData(record3Data.message, record3Data.dataBytes!); - await messageStore.put(alice.did, record3Data.message, additionalIndexes3); - await eventLog.append(alice.did, await Message.getCid(record3Data.message), additionalIndexes1); + const bobPrivateToAliceReply = await dwn.processMessage(alice.did, bobPrivateToAlice.message, { + dataStream: bobPrivateToAlice.dataStream + }); + expect(bobPrivateToAliceReply.status.code).to.equal(202); - const additionalIndexes4 = await record4Data.recordsWrite.constructIndexes(true); - record4Data.message = await recordsWriteHandler.cloneAndAddEncodedData(record4Data.message, record4Data.dataBytes!); - await messageStore.put(alice.did, record4Data.message, additionalIndexes4); - await eventLog.append(alice.did, await Message.getCid(record4Data.message), additionalIndexes1); + const bobPrivateToCarol = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const bobPrivateToCarolReply = await dwn.processMessage(alice.did, bobPrivateToCarol.message, { + dataStream: bobPrivateToCarol.dataStream + }); + expect(bobPrivateToCarolReply.status.code).to.equal(202); - const additionalIndexes5 = await record5Data.recordsWrite.constructIndexes(true); - record5Data.message = await recordsWriteHandler.cloneAndAddEncodedData(record5Data.message, record5Data.dataBytes!); - await messageStore.put(alice.did, record5Data.message, additionalIndexes5); - await eventLog.append(alice.did, await Message.getCid(record5Data.message), additionalIndexes1); + // write public records from alice to bob and carol + const alicePublicToBob = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const alicePublicToBobReply = await dwn.processMessage(alice.did, alicePublicToBob.message, { + dataStream: alicePublicToBob.dataStream + }); + expect(alicePublicToBobReply.status.code).to.equal(202); - // test correctness for Bob's query - const bobQueryMessageData = await TestDataGenerator.generateRecordsQuery({ + const alicePublicToCarol = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const alicePublicToCarolReply = await dwn.processMessage(alice.did, alicePublicToCarol.message, { + dataStream: alicePublicToCarol.dataStream + }); + expect(alicePublicToCarolReply.status.code).to.equal(202); + + // write public records from bob to alice and carol + const bobPublicToAlice = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const bobPublicToAliceReply = await dwn.processMessage(alice.did, bobPublicToAlice.message, { + dataStream: bobPublicToAlice.dataStream + }); + expect(bobPublicToAliceReply.status.code).to.equal(202); + + const bobPublicToCarol = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const bobPublicToCarolReply = await dwn.processMessage(alice.did, bobPublicToCarol.message, { + dataStream: bobPublicToCarol.dataStream + }); + expect(bobPublicToCarolReply.status.code).to.equal(202); + + // write public records from carol to alice and bob + const carolPublicToAlice = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const carolPublicToAliceReply = await dwn.processMessage(alice.did, carolPublicToAlice.message, { + dataStream: carolPublicToAlice.dataStream + }); + expect(carolPublicToAliceReply.status.code).to.equal(202); + + const carolPublicToBob = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const carolPublicToBobReply = await dwn.processMessage(alice.did, carolPublicToBob.message, { + dataStream: carolPublicToBob.dataStream + }); + expect(carolPublicToBobReply.status.code).to.equal(202); + + // bob queries for records with himself and alice as recipients + const bobQueryMessagesForBobAlice = await TestDataGenerator.generateRecordsQuery({ author : bob, - filter : { schema } + filter : { protocol: freeForAll.protocol, protocolPath: 'post', recipient: [bob.did, alice.did] } + }); + const bobQueryMessagesForBobAliceReply = await dwn.processMessage(alice.did, bobQueryMessagesForBobAlice.message); + expect(bobQueryMessagesForBobAliceReply.status.code).to.equal(200); + expect(bobQueryMessagesForBobAliceReply.entries?.length).to.equal(7); + + // Since Bob is the author if the query, we expect for him to be able to see: + // Private Messages THAT ANYONE sent to Bob + // Private Messages THAT ONLY HE sent to Alice + // Public Messages THAT ANYONE sent to Alice + // Public Messages THAT ANYONE sent to Bob + expect(bobQueryMessagesForBobAliceReply.entries!.map(e => e.recordId)).to.have.members([ + alicePrivateToBob.message.recordId, + carolPrivateToBob.message.recordId, + bobPrivateToAlice.message.recordId, + alicePublicToBob.message.recordId, + bobPublicToAlice.message.recordId, + carolPublicToAlice.message.recordId, + carolPublicToBob.message.recordId, + ]); + + // carol queries for records with herself as the recipient + const carolQueryMessagesForCarolAlice = await TestDataGenerator.generateRecordsQuery({ + author : carol, + filter : { protocol: freeForAll.protocol, protocolPath: 'post', recipient: carol.did } + }); + const carolQueryMessagesForCarolAliceReply = await dwn.processMessage(alice.did, carolQueryMessagesForCarolAlice.message); + expect(carolQueryMessagesForCarolAliceReply.status.code).to.equal(200); + expect(carolQueryMessagesForCarolAliceReply.entries?.length).to.equal(4); + + // Since Carol is the author if the query, we expect for her to be able to see: + // Private Messages THAT ANYONE sent to Carol + // Private Messages THAT ONLY SHE sent to Alice + // Public Messages THAT ANYONE sent to Alice + // Public Messages THAT ANYONE sent to Carol + expect(carolQueryMessagesForCarolAliceReply.entries!.map(e => e.recordId)).to.have.members([ + alicePrivateToCarol.message.recordId, + bobPrivateToCarol.message.recordId, + alicePublicToCarol.message.recordId, + bobPublicToCarol.message.recordId, + ]); + + // alice queries for ONLY published records with herself and bob as recipients + const aliceQueryPublished = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { protocol: freeForAll.protocol, protocolPath: 'post', recipient: [alice.did, bob.did], published: true } + }); + const aliceQueryPublishedReply = await dwn.processMessage(alice.did, aliceQueryPublished.message); + expect(aliceQueryPublishedReply.status.code).to.equal(200); + expect(aliceQueryPublishedReply.entries?.length).to.equal(4); + expect(aliceQueryPublishedReply.entries!.map(e => e.recordId)).to.have.members([ + alicePublicToBob.message.recordId, + carolPublicToBob.message.recordId, + bobPublicToAlice.message.recordId, + carolPublicToAlice.message.recordId, + ]); + + // carol queries for ONLY private records with herself and alice as the recipients + const carolQueryPrivate = await TestDataGenerator.generateRecordsQuery({ + author : carol, + filter : { protocol: freeForAll.protocol, protocolPath: 'post', recipient: [carol.did, alice.did], published: false } + }); + const carolQueryPrivateReply = await dwn.processMessage(alice.did, carolQueryPrivate.message); + expect(carolQueryPrivateReply.status.code).to.equal(200); + expect(carolQueryPrivateReply.entries?.length).to.equal(3); + // Carol can query for private messages she authored to alice, and her own private messages with herself as the recipient + expect(carolQueryPrivateReply.entries!.map(e => e.recordId)).to.have.members([ + alicePrivateToCarol.message.recordId, + bobPrivateToCarol.message.recordId, + carolPrivateToAlice.message.recordId, + ]); + }); + + it('should only return published records and unpublished records that are authored by specific author(s)', async () => { + // scenario: Alice installs a free-for-all protocol on her DWN + // She writes both private and public messages for bob and carol, carol and bob also write public and privet messages for alice and each other + // Bob, Alice and Carol should only be able to see private messages pertaining to themselves, and any public messages filtered by an author + // Bob, Alice and Carol should be able to filter for ONLY public messages or ONLY private messages + + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + const carol = await TestDataGenerator.generateDidKeyPersona(); + + // install the free-for-all protocol on Alice's DWN + const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : freeForAll }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); + expect(protocolConfigureReply.status.code).to.equal(202); - const replyToBob = await dwn.processMessage(alice.did, bobQueryMessageData.message); + // write private records for bob and carol + const alicePrivateToBob = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); - expect(replyToBob.status.code).to.equal(200); - expect(replyToBob.entries?.length).to.equal(4); // expect 4 records + const alicePrivateToBobReply = await dwn.processMessage(alice.did, alicePrivateToBob.message, { dataStream: alicePrivateToBob.dataStream }); + expect(alicePrivateToBobReply.status.code).to.equal(202); - const privateRecordsForBob = replyToBob.entries?.filter(message => message.encodedData === Encoder.stringToBase64Url('2'))!; - const privateRecordsFromBob = replyToBob.entries?.filter(message => message.encodedData === Encoder.stringToBase64Url('3'))!; - const publicRecords = replyToBob.entries?.filter(message => message.encodedData === Encoder.stringToBase64Url('4') || message.encodedData === Encoder.stringToBase64Url('5'))!; - expect(privateRecordsForBob.length).to.equal(1); - expect(privateRecordsFromBob.length).to.equal(1); - expect(publicRecords.length).to.equal(2); + const alicePrivateToCarol = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const alicePrivateToCarolReply = await dwn.processMessage(alice.did, alicePrivateToCarol.message, { + dataStream: alicePrivateToCarol.dataStream + }); + expect(alicePrivateToCarolReply.status.code).to.equal(202); + + // write private records from carol to alice and bob + const carolPrivateToAlice = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const carolPrivateToAliceReply = await dwn.processMessage(alice.did, carolPrivateToAlice.message, { + dataStream: carolPrivateToAlice.dataStream + }); + expect(carolPrivateToAliceReply.status.code).to.equal(202); + + const carolPrivateToBob = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); + const carolPrivateToBobReply = await dwn.processMessage(alice.did, carolPrivateToBob.message, { + dataStream: carolPrivateToBob.dataStream + }); + expect(carolPrivateToBobReply.status.code).to.equal(202); + + // write private records from bob to alice and carol + const bobPrivateToAlice = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }); - // check for explicitly published:false records for Bob - const bobQueryPublishedFalse = await TestDataGenerator.generateRecordsQuery({ - author : bob, - filter : { schema, published: false } + const bobPrivateToAliceReply = await dwn.processMessage(alice.did, bobPrivateToAlice.message, { + dataStream: bobPrivateToAlice.dataStream }); - const unpublishedBobReply = await dwn.processMessage(alice.did, bobQueryPublishedFalse.message); - expect(unpublishedBobReply.status.code).to.equal(200); - expect(unpublishedBobReply.entries?.length).to.equal(2); - const unpublishedBobRecordIds = unpublishedBobReply.entries?.map(e => e.recordId); - expect(unpublishedBobRecordIds).to.have.members([ record2Data.message.recordId, record3Data.message.recordId ]); + expect(bobPrivateToAliceReply.status.code).to.equal(202); - // test correctness for Alice's query - const aliceQueryMessageData = await TestDataGenerator.generateRecordsQuery({ - author : alice, - filter : { schema } + const bobPrivateToCarol = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], }); + const bobPrivateToCarolReply = await dwn.processMessage(alice.did, bobPrivateToCarol.message, { + dataStream: bobPrivateToCarol.dataStream + }); + expect(bobPrivateToCarolReply.status.code).to.equal(202); - const replyToAliceQuery = await dwn.processMessage(alice.did, aliceQueryMessageData.message); + // write public records from alice to bob and carol + const alicePublicToBob = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const alicePublicToBobReply = await dwn.processMessage(alice.did, alicePublicToBob.message, { + dataStream: alicePublicToBob.dataStream + }); + expect(alicePublicToBobReply.status.code).to.equal(202); - expect(replyToAliceQuery.status.code).to.equal(200); - expect(replyToAliceQuery.entries?.length).to.equal(5); // expect all 5 records + const alicePublicToCarol = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const alicePublicToCarolReply = await dwn.processMessage(alice.did, alicePublicToCarol.message, { + dataStream: alicePublicToCarol.dataStream + }); + expect(alicePublicToCarolReply.status.code).to.equal(202); - // filter for public records with carol as recipient - const bobQueryCarolMessageData = await TestDataGenerator.generateRecordsQuery({ - author : bob, - filter : { schema, recipient: carol.did } + // write public records from bob to alice and carol + const bobPublicToAlice = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const bobPublicToAliceReply = await dwn.processMessage(alice.did, bobPublicToAlice.message, { + dataStream: bobPublicToAlice.dataStream }); - const replyToBobCarolQuery = await dwn.processMessage(alice.did, bobQueryCarolMessageData.message); - expect(replyToBobCarolQuery.status.code).to.equal(200); - expect(replyToBobCarolQuery.entries?.length).to.equal(1); - expect(replyToBobCarolQuery.entries![0]!.encodedData).to.equal(Encoder.stringToBase64Url('5')); + expect(bobPublicToAliceReply.status.code).to.equal(202); - // filter for explicit unpublished public records with carol as recipient, should not return any. - const bobQueryCarolMessageDataUnpublished = await TestDataGenerator.generateRecordsQuery({ + const bobPublicToCarol = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : carol.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const bobPublicToCarolReply = await dwn.processMessage(alice.did, bobPublicToCarol.message, { + dataStream: bobPublicToCarol.dataStream + }); + expect(bobPublicToCarolReply.status.code).to.equal(202); + + // write public records from carol to alice and bob + const carolPublicToAlice = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : alice.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const carolPublicToAliceReply = await dwn.processMessage(alice.did, carolPublicToAlice.message, { + dataStream: carolPublicToAlice.dataStream + }); + expect(carolPublicToAliceReply.status.code).to.equal(202); + + const carolPublicToBob = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : bob.did, + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + published : true + }); + const carolPublicToBobReply = await dwn.processMessage(alice.did, carolPublicToBob.message, { + dataStream: carolPublicToBob.dataStream + }); + expect(carolPublicToBobReply.status.code).to.equal(202); + + // bob queries for records with himself and alice as authors + const bobQueryMessagesForBobAlice = await TestDataGenerator.generateRecordsQuery({ author : bob, - filter : { schema, recipient: carol.did, published: false } - }); - const replyToBobCarolUnpublishedQuery = await dwn.processMessage(alice.did, bobQueryCarolMessageDataUnpublished.message); - expect(replyToBobCarolUnpublishedQuery.status.code).to.equal(200); - expect(replyToBobCarolUnpublishedQuery.entries?.length).to.equal(0); + filter : { protocol: freeForAll.protocol, protocolPath: 'post', author: [bob.did, alice.did] } + }); + const bobQueryMessagesForBobAliceReply = await dwn.processMessage(alice.did, bobQueryMessagesForBobAlice.message); + expect(bobQueryMessagesForBobAliceReply.status.code).to.equal(200); + expect(bobQueryMessagesForBobAliceReply.entries?.length).to.equal(7); + + // Since Bob is the author if the query, we expect for him to be able to see: + // Private Messages Bob authored TO ANYONE + // Private Messages Alice authored To Bob + // Public Messages Alice authored + // Public Messages Bob authored + expect(bobQueryMessagesForBobAliceReply.entries!.map(e => e.recordId)).to.have.members([ + alicePrivateToBob.message.recordId, + bobPrivateToAlice.message.recordId, + bobPrivateToCarol.message.recordId, + alicePublicToBob.message.recordId, + alicePublicToCarol.message.recordId, + bobPublicToAlice.message.recordId, + bobPublicToCarol.message.recordId + ]); + + // carol queries for records with herself as the author + const carolQueryMessagesForCarolAlice = await TestDataGenerator.generateRecordsQuery({ + author : carol, + filter : { protocol: freeForAll.protocol, protocolPath: 'post', author: carol.did } + }); + const carolQueryMessagesForCarolAliceReply = await dwn.processMessage(alice.did, carolQueryMessagesForCarolAlice.message); + expect(carolQueryMessagesForCarolAliceReply.status.code).to.equal(200); + expect(carolQueryMessagesForCarolAliceReply.entries?.length).to.equal(4); + + // Since Carol is the author if the query, we expect for her to be able to see: + // All messages that Carol sent to anyone, private or public + expect(carolQueryMessagesForCarolAliceReply.entries!.map(e => e.recordId)).to.have.members([ + carolPrivateToAlice.message.recordId, + carolPrivateToBob.message.recordId, + carolPublicToAlice.message.recordId, + carolPublicToBob.message.recordId + ]); + + // alice queries for ONLY published records with herself and bob as authors + const aliceQueryPublished = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { protocol: freeForAll.protocol, protocolPath: 'post', author: [alice.did, bob.did], published: true } + }); + const aliceQueryPublishedReply = await dwn.processMessage(alice.did, aliceQueryPublished.message); + expect(aliceQueryPublishedReply.status.code).to.equal(200); + expect(aliceQueryPublishedReply.entries?.length).to.equal(4); + expect(aliceQueryPublishedReply.entries!.map(e => e.recordId)).to.have.members([ + alicePublicToBob.message.recordId, + alicePublicToCarol.message.recordId, + bobPublicToAlice.message.recordId, + bobPublicToCarol.message.recordId + ]); + + // carol queries for ONLY private records with herself and alice as the authors + const carolQueryPrivate = await TestDataGenerator.generateRecordsQuery({ + author : carol, + filter : { protocol: freeForAll.protocol, protocolPath: 'post', author: [carol.did, alice.did], published: false } + }); + const carolQueryPrivateReply = await dwn.processMessage(alice.did, carolQueryPrivate.message); + expect(carolQueryPrivateReply.status.code).to.equal(200); + expect(carolQueryPrivateReply.entries?.length).to.equal(3); + expect(carolQueryPrivateReply.entries!.map(e => e.recordId)).to.have.members([ + alicePrivateToCarol.message.recordId, + carolPrivateToAlice.message.recordId, + carolPrivateToBob.message.recordId + ]); }); it('should paginate correctly for fetchRecordsAsNonOwner()', async () => { diff --git a/tests/scenarios/aggregator.spec.ts b/tests/scenarios/aggregator.spec.ts new file mode 100644 index 000000000..dd030c5c8 --- /dev/null +++ b/tests/scenarios/aggregator.spec.ts @@ -0,0 +1,747 @@ +import type { DidResolver } from '@web5/dids'; +import type { EventStream } from '../../src/types/subscriptions.js'; +import { type DataStore, DataStream, type EventLog, type MessageStore, type ProtocolDefinition, type ResumableTaskStore } from '../../src/index.js'; + +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import chai, { expect } from 'chai'; + +import { Dwn } from '../../src/dwn.js'; +import { Jws } from '../../src/utils/jws.js'; +import { ProtocolsConfigure } from '../../src/interfaces/protocols-configure.js'; +import { RecordsQuery } from '../../src/interfaces/records-query.js'; +import { RecordsWrite } from '../../src/interfaces/records-write.js'; +import { TestDataGenerator } from '../utils/test-data-generator.js'; +import { TestEventStream } from '../test-event-stream.js'; +import { TestStores } from '../test-stores.js'; +import { DidKey, UniversalResolver } from '@web5/dids'; + +chai.use(chaiAsPromised); + +// This is a test suite that demonstrates how to use the DWN to create aggregators +// Aggregators allows multiple authors to write records to the aggregator's DID based on a role +// +// NOTE: This will be more evident when we introduce `signWithRole`. +// This would allow writing to your local DWN without any role field, but when writing to an aggregator, you could conform to their own roles. +describe('Aggregator Model', () => { + let didResolver: DidResolver; + let messageStore: MessageStore; + let dataStore: DataStore; + let resumableTaskStore: ResumableTaskStore; + let eventLog: EventLog; + let eventStream: EventStream; + let dwn: Dwn; + + const protocol = 'https://example.org/notes'; + + // A simple protocol for the user that only allows them to write or read their own notes + const userProtocolDefinition:ProtocolDefinition = { + protocol, + published : true, + types : { + note: { + schema : 'https://example.org/note', + dataFormats : ['text/plain', 'application/json'], + } + }, + structure: { + note: {} + } + }; + + // A simple protocol that allows members of an aggregator to write notes to the aggregator + // Anyone can query or read public notes, the rest of the notes are enforced by `recipient/author` rules. + const aggregatorProtocolDefinition:ProtocolDefinition = { + protocol, + published : true, + types : { + note: { + schema : 'https://example.org/note', + dataFormats : ['text/plain', 'application/json'], + }, + member: { + schema : 'https://example.org/member', + dataFormats : ['application/json'], + } + }, + structure: { + member: { + $role: true, + }, + note: { + $actions: [{ + role : 'member', + can : ['create', 'update', 'delete'] + }] + } + } + }; + + // important to follow the `before` and `after` pattern to initialize and clean the stores in tests + // so that different test suites can reuse the same backend store for testing + before(async () => { + didResolver = new UniversalResolver({ didResolvers: [DidKey] }); + + const stores = TestStores.get(); + messageStore = stores.messageStore; + dataStore = stores.dataStore; + resumableTaskStore = stores.resumableTaskStore; + eventLog = stores.eventLog; + eventStream = TestEventStream.get(); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore, eventLog, eventStream, resumableTaskStore }); + }); + + beforeEach(async () => { + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + + // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + await messageStore.clear(); + await dataStore.clear(); + await resumableTaskStore.clear(); + await eventLog.clear(); + }); + + after(async () => { + await dwn.close(); + }); + + it('should support querying from multiple authors', async () => { + // scenario: Alice, Bob, Carol are members of an aggregator. + // Alice writes a note to Carol, Bob writes a note to Alice, Carol writes a note to Bob. + // Daniel is not a member of the aggregator and tries to unsuccessfully write a note to Alice. + // Daniel can query public notes from multiple authors in a single query. + // Alice and Bob create private notes with Carol as the recipient. + // Bob creates a private note to Alice. + // Daniel does not see the private notes in his query. + // Carol can see all notes from Alice and Bob in her query, including the private notes intended for her. + // Alice can see all notes from Bob and Carol in her query, including the private notes intended for her. + + // create aggregator DID and install aggregator note protocol + const aggregator = await TestDataGenerator.generateDidKeyPersona(); + const aggregatorProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(aggregator), + definition : aggregatorProtocolDefinition, + }); + const aggregatorProtocolReply = await dwn.processMessage(aggregator.did, aggregatorProtocolConfigure.message); + expect(aggregatorProtocolReply.status.code).to.equal(202, 'aggregator configure'); + + // create 4 users and install user note protocol + const alice = await TestDataGenerator.generateDidKeyPersona(); + const aliceProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(alice), + definition : userProtocolDefinition, + }); + const aliceProtocolReply = await dwn.processMessage(alice.did, aliceProtocolConfigure.message); + expect(aliceProtocolReply.status.code).to.equal(202, 'alice configure'); + + const bob = await TestDataGenerator.generateDidKeyPersona(); + const bobProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(bob), + definition : userProtocolDefinition, + }); + const bobProtocolReply = await dwn.processMessage(bob.did, bobProtocolConfigure.message); + expect(bobProtocolReply.status.code).to.equal(202, 'bob configure'); + + const carol = await TestDataGenerator.generateDidKeyPersona(); + const carolProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(carol), + definition : userProtocolDefinition, + }); + const carolProtocolReply = await dwn.processMessage(carol.did, carolProtocolConfigure.message); + expect(carolProtocolReply.status.code).to.equal(202, 'carol configure'); + + const daniel = await TestDataGenerator.generateDidKeyPersona(); + const danielProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(daniel), + definition : userProtocolDefinition, + }); + const danielProtocolReply = await dwn.processMessage(daniel.did, danielProtocolConfigure.message); + expect(danielProtocolReply.status.code).to.equal(202, 'daniel configure'); + + + // The aggregator creates member records for alice, bob and carol + + const aliceMemberData = TestDataGenerator.randomBytes(256); + const aliceMember = await RecordsWrite.create({ + signer : Jws.createSigner(aggregator), + recipient : alice.did, + protocol : userProtocolDefinition.protocol, + protocolPath : 'member', + schema : 'https://example.org/member', + dataFormat : 'application/json', + data : aliceMemberData, + }); + const aliceMemberReply = await dwn.processMessage(aggregator.did, aliceMember.message, { dataStream: DataStream.fromBytes(aliceMemberData) }); + expect(aliceMemberReply.status.code).to.equal(202, 'alice member ' + aliceMemberReply.status.detail); + + const bobMemberData = TestDataGenerator.randomBytes(256); + const bobMember = await RecordsWrite.create({ + signer : Jws.createSigner(aggregator), + recipient : bob.did, + protocol : userProtocolDefinition.protocol, + protocolPath : 'member', + schema : 'https://example.org/member', + dataFormat : 'application/json', + data : bobMemberData, + }); + const bobMemberReply = await dwn.processMessage(aggregator.did, bobMember.message, { dataStream: DataStream.fromBytes(bobMemberData) }); + expect(bobMemberReply.status.code).to.equal(202, 'bob member'); + + const carolMemberData = TestDataGenerator.randomBytes(256); + const carolMember = await RecordsWrite.create({ + signer : Jws.createSigner(aggregator), + recipient : carol.did, + protocol : userProtocolDefinition.protocol, + protocolPath : 'member', + schema : 'https://example.org/member', + dataFormat : 'application/json', + data : carolMemberData, + }); + + const carolMemberReply = await dwn.processMessage(aggregator.did, carolMember.message, { dataStream: DataStream.fromBytes(carolMemberData) }); + expect(carolMemberReply.status.code).to.equal(202, 'carol member'); + + // alice writes a public note to carol and posts it in the aggregator + const aliceNoteData = TestDataGenerator.randomBytes(256); + const aliceNoteToCarol = await RecordsWrite.create({ + signer : Jws.createSigner(alice), + recipient : carol.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : aliceNoteData, + protocolRole : 'member' + }); + + // Alice writes it to her own DWN and the aggregator + const aliceLocalDWN = await dwn.processMessage(alice.did, aliceNoteToCarol.message, { dataStream: DataStream.fromBytes(aliceNoteData) }); + expect(aliceLocalDWN.status.code).to.equal(202, 'alice note'); + const aliceAggregatorDWN = await dwn.processMessage(aggregator.did, aliceNoteToCarol.message, { + dataStream: DataStream.fromBytes(aliceNoteData) + }); + expect(aliceAggregatorDWN.status.code).to.equal(202, 'alice note aggregator'); + + // bob writes a public note to alice and posts it in the aggregator + const bobNoteToAliceData = TestDataGenerator.randomBytes(256); + const bobNoteToAlice = await RecordsWrite.create({ + signer : Jws.createSigner(bob), + recipient : alice.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : bobNoteToAliceData, + protocolRole : 'member' + }); + + // Bob writes it to his own DWN and the aggregator + const bobLocalDWN = await dwn.processMessage(bob.did, bobNoteToAlice.message, { dataStream: DataStream.fromBytes(bobNoteToAliceData) }); + expect(bobLocalDWN.status.code).to.equal(202, 'bob note'); + const bobAggregatorDWN = await dwn.processMessage(aggregator.did, bobNoteToAlice.message, { + dataStream: DataStream.fromBytes(bobNoteToAliceData) + }); + expect(bobAggregatorDWN.status.code).to.equal(202, 'bob note aggregator'); + + // carol writes a public note to bob and posts it in the aggregator + const carolNoteToBobData = TestDataGenerator.randomBytes(256); + const carolNoteToBob = await RecordsWrite.create({ + signer : Jws.createSigner(carol), + recipient : bob.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : carolNoteToBobData, + protocolRole : 'member' + }); + + // Carol writes it to her own DWN and the aggregator + const carolLocalDWN = await dwn.processMessage(carol.did, carolNoteToBob.message, { + dataStream: DataStream.fromBytes(carolNoteToBobData) + }); + expect(carolLocalDWN.status.code).to.equal(202, 'carol note'); + const carolAggregatorDWN = await dwn.processMessage(aggregator.did, carolNoteToBob.message, { + dataStream: DataStream.fromBytes(carolNoteToBobData) + }); + expect(carolAggregatorDWN.status.code).to.equal(202, 'carol note aggregator'); + + // daniel writes a public note to alice and posts it in the aggregator (which will reject it as he is not a member) + const danielNoteToAlice = TestDataGenerator.randomBytes(256); + const danielNote = await RecordsWrite.create({ + signer : Jws.createSigner(daniel), + recipient : alice.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : danielNoteToAlice, + protocolRole : 'member' + }); + + // Daniel writes it to his own DWN and the aggregator + const danielLocalDWN = await dwn.processMessage(daniel.did, danielNote.message, { dataStream: DataStream.fromBytes(danielNoteToAlice) }); + expect(danielLocalDWN.status.code).to.equal(202, 'daniel note'); + const danielAggregatorDWN = await dwn.processMessage(aggregator.did, danielNote.message, { dataStream: DataStream.fromBytes(danielNoteToAlice) }); + expect(danielAggregatorDWN.status.code).to.equal(401, 'daniel note aggregator'); + + + // daniel can read public notes from multiple authors in a single query + const danielRead = await RecordsQuery.create({ + signer : Jws.createSigner(daniel), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + author : [alice.did, bob.did], + } + }); + + const danielReadReply = await dwn.processMessage(aggregator.did, danielRead.message); + expect(danielReadReply.status.code).to.equal(200, 'daniel read'); + expect(danielReadReply.entries?.length).to.equal(2, 'daniel read records'); + expect(danielReadReply.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'daniel read alice note'); + expect(danielReadReply.entries![1].recordId).to.equal(bobNoteToAlice.message.recordId, 'daniel read bob note'); + + // create private notes to carol from alice and bob + const alicePrivateNoteToCarol = TestDataGenerator.randomBytes(256); + const aliceNoteToCarolPrivate = await RecordsWrite.create({ + signer : Jws.createSigner(alice), + recipient : carol.did, + published : false, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : alicePrivateNoteToCarol, + protocolRole : 'member' + }); + + const aliceNoteToCarolLocal = await dwn.processMessage(alice.did, aliceNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(alicePrivateNoteToCarol) + }); + expect(aliceNoteToCarolLocal.status.code).to.equal(202, 'alice private note'); + + const aliceNoteToCarolAggregator = await dwn.processMessage(aggregator.did, aliceNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(alicePrivateNoteToCarol) + }); + expect(aliceNoteToCarolAggregator.status.code).to.equal(202, 'alice private note aggregator'); + + const bobPrivateNoteToCarol = TestDataGenerator.randomBytes(256); + const bobNoteToCarolPrivate = await RecordsWrite.create({ + signer : Jws.createSigner(bob), + recipient : carol.did, + published : false, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : bobPrivateNoteToCarol, + protocolRole : 'member' + }); + + const bobNoteToCarolLocal = await dwn.processMessage(bob.did, bobNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(bobPrivateNoteToCarol) + }); + expect(bobNoteToCarolLocal.status.code).to.equal(202, 'bob private note'); + + const bobNoteToCarolAggregator = await dwn.processMessage(aggregator.did, bobNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(bobPrivateNoteToCarol) + }); + expect(bobNoteToCarolAggregator.status.code).to.equal(202, 'bob private note aggregator'); + + // create a private note from bob to alice + const bobNoteToAlicePrivateData = TestDataGenerator.randomBytes(256); + const bobNoteToAlicePrivate = await RecordsWrite.create({ + signer : Jws.createSigner(bob), + recipient : alice.did, + published : false, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : bobNoteToAlicePrivateData, + protocolRole : 'member' + }); + + const bobNoteToAliceLocal = await dwn.processMessage(bob.did, bobNoteToAlicePrivate.message, { + dataStream: DataStream.fromBytes(bobNoteToAlicePrivateData) + }); + expect(bobNoteToAliceLocal.status.code).to.equal(202, 'alice private note to bob'); + const bobNoteToAliceAggregator = await dwn.processMessage(aggregator.did, bobNoteToAlicePrivate.message, { + dataStream: DataStream.fromBytes(bobNoteToAlicePrivateData) + }); + expect(bobNoteToAliceAggregator.status.code).to.equal(202, 'alice private note to bob aggregator'); + + // confirm daniel can still only read the public notes + const danielRead2 = await RecordsQuery.create({ + signer : Jws.createSigner(daniel), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + author : [alice.did, bob.did], + } + }); + + const danielReadReply2 = await dwn.processMessage(aggregator.did, danielRead2.message); + expect(danielReadReply2.status.code).to.equal(200, 'daniel read 2'); + expect(danielReadReply2.entries?.length).to.equal(2, 'daniel read records 2'); + expect(danielReadReply2.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'daniel read alice note 2'); + expect(danielReadReply2.entries![1].recordId).to.equal(bobNoteToAlice.message.recordId, 'daniel read bob note 2'); + + // carol queries for notes from alice and bob and gets the public notes and private notes destined for her + // carol does not see the private note from alice to bob + const carolRead = await RecordsQuery.create({ + signer : Jws.createSigner(carol), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + author : [alice.did, bob.did], + } + }); + + const carolReadReply = await dwn.processMessage(aggregator.did, carolRead.message); + expect(carolReadReply.status.code).to.equal(200, 'carol read'); + expect(carolReadReply.entries?.length).to.equal(4, 'carol read records'); + expect(carolReadReply.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'carol read alice note'); + expect(carolReadReply.entries![1].recordId).to.equal(bobNoteToAlice.message.recordId, 'carol read bob note'); + expect(carolReadReply.entries![2].recordId).to.equal(aliceNoteToCarolPrivate.message.recordId, 'carol read alice private note'); + expect(carolReadReply.entries![3].recordId).to.equal(bobNoteToCarolPrivate.message.recordId, 'carol read bob private note'); + + // alice queries for notes from bob and carol and gets the public notes and private notes destined for her + const aliceRead = await RecordsQuery.create({ + signer : Jws.createSigner(alice), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + author : [bob.did, carol.did], + } + }); + + const aliceReadReply = await dwn.processMessage(aggregator.did, aliceRead.message); + expect(aliceReadReply.status.code).to.equal(200, 'alice read'); + expect(aliceReadReply.entries?.length).to.equal(3, 'alice read records'); + expect(aliceReadReply.entries![0].recordId).to.equal(bobNoteToAlice.message.recordId, 'alice note to carol public'); + expect(aliceReadReply.entries![1].recordId).to.equal(carolNoteToBob.message.recordId, 'carol note to bob public'); + expect(aliceReadReply.entries![2].recordId).to.equal(bobNoteToAlicePrivate.message.recordId, 'bob note to alice private'); + }); + + it('should support querying from multiple recipients', async () => { + + // create aggregator DID and install aggregator note protocol + const aggregator = await TestDataGenerator.generateDidKeyPersona(); + const aggregatorProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(aggregator), + definition : aggregatorProtocolDefinition, + }); + const aggregatorProtocolReply = await dwn.processMessage(aggregator.did, aggregatorProtocolConfigure.message); + expect(aggregatorProtocolReply.status.code).to.equal(202, 'aggregator configure'); + + // create 4 users and install user note protocol + const alice = await TestDataGenerator.generateDidKeyPersona(); + const aliceProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(alice), + definition : userProtocolDefinition, + }); + const aliceProtocolReply = await dwn.processMessage(alice.did, aliceProtocolConfigure.message); + expect(aliceProtocolReply.status.code).to.equal(202, 'alice configure'); + + const bob = await TestDataGenerator.generateDidKeyPersona(); + const bobProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(bob), + definition : userProtocolDefinition, + }); + const bobProtocolReply = await dwn.processMessage(bob.did, bobProtocolConfigure.message); + expect(bobProtocolReply.status.code).to.equal(202, 'bob configure'); + + const carol = await TestDataGenerator.generateDidKeyPersona(); + const carolProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(carol), + definition : userProtocolDefinition, + }); + const carolProtocolReply = await dwn.processMessage(carol.did, carolProtocolConfigure.message); + expect(carolProtocolReply.status.code).to.equal(202, 'carol configure'); + + const daniel = await TestDataGenerator.generateDidKeyPersona(); + const danielProtocolConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(daniel), + definition : userProtocolDefinition, + }); + const danielProtocolReply = await dwn.processMessage(daniel.did, danielProtocolConfigure.message); + expect(danielProtocolReply.status.code).to.equal(202, 'daniel configure'); + + + // The aggregator creates member records for alice, bob and carol + + const aliceMemberData = TestDataGenerator.randomBytes(256); + const aliceMember = await RecordsWrite.create({ + signer : Jws.createSigner(aggregator), + recipient : alice.did, + protocol : userProtocolDefinition.protocol, + protocolPath : 'member', + schema : 'https://example.org/member', + dataFormat : 'application/json', + data : aliceMemberData, + }); + const aliceMemberReply = await dwn.processMessage(aggregator.did, aliceMember.message, { dataStream: DataStream.fromBytes(aliceMemberData) }); + expect(aliceMemberReply.status.code).to.equal(202, 'alice member ' + aliceMemberReply.status.detail); + + const bobMemberData = TestDataGenerator.randomBytes(256); + const bobMember = await RecordsWrite.create({ + signer : Jws.createSigner(aggregator), + recipient : bob.did, + protocol : userProtocolDefinition.protocol, + protocolPath : 'member', + schema : 'https://example.org/member', + dataFormat : 'application/json', + data : bobMemberData, + }); + const bobMemberReply = await dwn.processMessage(aggregator.did, bobMember.message, { dataStream: DataStream.fromBytes(bobMemberData) }); + expect(bobMemberReply.status.code).to.equal(202, 'bob member'); + + const carolMemberData = TestDataGenerator.randomBytes(256); + const carolMember = await RecordsWrite.create({ + signer : Jws.createSigner(aggregator), + recipient : carol.did, + protocol : userProtocolDefinition.protocol, + protocolPath : 'member', + schema : 'https://example.org/member', + dataFormat : 'application/json', + data : carolMemberData, + }); + + const carolMemberReply = await dwn.processMessage(aggregator.did, carolMember.message, { dataStream: DataStream.fromBytes(carolMemberData) }); + expect(carolMemberReply.status.code).to.equal(202, 'carol member'); + + // alice writes a public note to carol and posts it in the aggregator + const aliceNoteData = TestDataGenerator.randomBytes(256); + const aliceNoteToCarol = await RecordsWrite.create({ + signer : Jws.createSigner(alice), + recipient : carol.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : aliceNoteData, + protocolRole : 'member' + }); + + // Alice writes it to her own DWN and the aggregator + const aliceLocalDWN = await dwn.processMessage(alice.did, aliceNoteToCarol.message, { dataStream: DataStream.fromBytes(aliceNoteData) }); + expect(aliceLocalDWN.status.code).to.equal(202, 'alice note'); + const aliceAggregatorDWN = await dwn.processMessage(aggregator.did, aliceNoteToCarol.message, { + dataStream: DataStream.fromBytes(aliceNoteData) + }); + expect(aliceAggregatorDWN.status.code).to.equal(202, 'alice note aggregator'); + + // bob writes a public note to alice and posts it in the aggregator + const bobNoteToAliceData = TestDataGenerator.randomBytes(256); + const bobNoteToAlice = await RecordsWrite.create({ + signer : Jws.createSigner(bob), + recipient : alice.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : bobNoteToAliceData, + protocolRole : 'member' + }); + + // Bob writes it to his own DWN and the aggregator + const bobLocalDWN = await dwn.processMessage(bob.did, bobNoteToAlice.message, { dataStream: DataStream.fromBytes(bobNoteToAliceData) }); + expect(bobLocalDWN.status.code).to.equal(202, 'bob note'); + const bobAggregatorDWN = await dwn.processMessage(aggregator.did, bobNoteToAlice.message, { + dataStream: DataStream.fromBytes(bobNoteToAliceData) + }); + expect(bobAggregatorDWN.status.code).to.equal(202, 'bob note aggregator'); + + // carol writes a public note to bob and posts it in the aggregator + const carolNoteToBobData = TestDataGenerator.randomBytes(256); + const carolNoteToBob = await RecordsWrite.create({ + signer : Jws.createSigner(carol), + recipient : bob.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : carolNoteToBobData, + protocolRole : 'member' + }); + + // Carol writes it to her own DWN and the aggregator + const carolLocalDWN = await dwn.processMessage(carol.did, carolNoteToBob.message, { + dataStream: DataStream.fromBytes(carolNoteToBobData) + }); + expect(carolLocalDWN.status.code).to.equal(202, 'carol note'); + const carolAggregatorDWN = await dwn.processMessage(aggregator.did, carolNoteToBob.message, { + dataStream: DataStream.fromBytes(carolNoteToBobData) + }); + expect(carolAggregatorDWN.status.code).to.equal(202, 'carol note aggregator'); + + // daniel writes a public note to alice and posts it in the aggregator (which will reject it as he is not a member) + const danielNoteToAlice = TestDataGenerator.randomBytes(256); + const danielNote = await RecordsWrite.create({ + signer : Jws.createSigner(daniel), + recipient : alice.did, + published : true, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : danielNoteToAlice, + protocolRole : 'member' + }); + + // Daniel writes it to his own DWN and the aggregator + const danielLocalDWN = await dwn.processMessage(daniel.did, danielNote.message, { dataStream: DataStream.fromBytes(danielNoteToAlice) }); + expect(danielLocalDWN.status.code).to.equal(202, 'daniel note'); + const danielAggregatorDWN = await dwn.processMessage(aggregator.did, danielNote.message, { dataStream: DataStream.fromBytes(danielNoteToAlice) }); + expect(danielAggregatorDWN.status.code).to.equal(401, 'daniel note aggregator'); + + + // daniel can read public notes from multiple authors in a single query + const danielRead = await RecordsQuery.create({ + signer : Jws.createSigner(daniel), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + recipient : [ alice.did, carol.did ], + } + }); + + const danielReadReply = await dwn.processMessage(aggregator.did, danielRead.message); + expect(danielReadReply.status.code).to.equal(200, 'daniel read'); + expect(danielReadReply.entries?.length).to.equal(2, 'daniel read records'); + expect(danielReadReply.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'daniel read alice note'); + expect(danielReadReply.entries![1].recordId).to.equal(bobNoteToAlice.message.recordId, 'daniel read bob note'); + + // create private notes to carol from alice and bob + const alicePrivateNoteToCarol = TestDataGenerator.randomBytes(256); + const aliceNoteToCarolPrivate = await RecordsWrite.create({ + signer : Jws.createSigner(alice), + recipient : carol.did, + published : false, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : alicePrivateNoteToCarol, + protocolRole : 'member' + }); + + const aliceNoteToCarolLocal = await dwn.processMessage(alice.did, aliceNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(alicePrivateNoteToCarol) + }); + expect(aliceNoteToCarolLocal.status.code).to.equal(202, 'alice private note'); + + const aliceNoteToCarolAggregator = await dwn.processMessage(aggregator.did, aliceNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(alicePrivateNoteToCarol) + }); + expect(aliceNoteToCarolAggregator.status.code).to.equal(202, 'alice private note aggregator'); + + const bobPrivateNoteToCarol = TestDataGenerator.randomBytes(256); + const bobNoteToCarolPrivate = await RecordsWrite.create({ + signer : Jws.createSigner(bob), + recipient : carol.did, + published : false, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : bobPrivateNoteToCarol, + protocolRole : 'member' + }); + + const bobNoteToCarolLocal = await dwn.processMessage(bob.did, bobNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(bobPrivateNoteToCarol) + }); + expect(bobNoteToCarolLocal.status.code).to.equal(202, 'bob private note'); + + const bobNoteToCarolAggregator = await dwn.processMessage(aggregator.did, bobNoteToCarolPrivate.message, { + dataStream: DataStream.fromBytes(bobPrivateNoteToCarol) + }); + expect(bobNoteToCarolAggregator.status.code).to.equal(202, 'bob private note aggregator'); + + // create a private note from bob to alice + const bobNoteToAlicePrivateData = TestDataGenerator.randomBytes(256); + const bobNoteToAlicePrivate = await RecordsWrite.create({ + signer : Jws.createSigner(bob), + recipient : alice.did, + published : false, + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + dataFormat : 'application/json', + schema : 'https://example.org/note', + data : bobNoteToAlicePrivateData, + protocolRole : 'member' + }); + + const bobNoteToAliceLocal = await dwn.processMessage(bob.did, bobNoteToAlicePrivate.message, { + dataStream: DataStream.fromBytes(bobNoteToAlicePrivateData) + }); + expect(bobNoteToAliceLocal.status.code).to.equal(202, 'alice private note to bob'); + const bobNoteToAliceAggregator = await dwn.processMessage(aggregator.did, bobNoteToAlicePrivate.message, { + dataStream: DataStream.fromBytes(bobNoteToAlicePrivateData) + }); + expect(bobNoteToAliceAggregator.status.code).to.equal(202, 'alice private note to bob aggregator'); + + // confirm daniel can still only read the public notes + const danielRead2 = await RecordsQuery.create({ + signer : Jws.createSigner(daniel), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + recipient : [ alice.did, carol.did ], + } + }); + + const danielReadReply2 = await dwn.processMessage(aggregator.did, danielRead2.message); + expect(danielReadReply2.status.code).to.equal(200, 'daniel read 2'); + expect(danielReadReply2.entries?.length).to.equal(2, 'daniel read records 2'); + expect(danielReadReply2.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'daniel read alice note 2'); + expect(danielReadReply2.entries![1].recordId).to.equal(bobNoteToAlice.message.recordId, 'daniel read bob note 2'); + + // carol queries for notes from alice and bob and gets the public notes and private notes destined for her + // carol does not see the private note from alice to bob + const carolRead = await RecordsQuery.create({ + signer : Jws.createSigner(carol), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + recipient : [ alice.did, carol.did ], + } + }); + + const carolReadReply = await dwn.processMessage(aggregator.did, carolRead.message); + expect(carolReadReply.status.code).to.equal(200, 'carol read'); + expect(carolReadReply.entries?.length).to.equal(4, 'carol read records'); + expect(carolReadReply.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'carol read alice note'); + expect(carolReadReply.entries![1].recordId).to.equal(bobNoteToAlice.message.recordId, 'carol read bob note'); + expect(carolReadReply.entries![2].recordId).to.equal(aliceNoteToCarolPrivate.message.recordId, 'carol read alice private note'); + expect(carolReadReply.entries![3].recordId).to.equal(bobNoteToCarolPrivate.message.recordId, 'carol read bob private note'); + + // alice queries for notes from bob and carol and gets the public notes and private notes destined for her + const aliceRead = await RecordsQuery.create({ + signer : Jws.createSigner(alice), + filter : { + protocol : userProtocolDefinition.protocol, + protocolPath : 'note', + recipient : [ carol.did, bob.did ], + } + }); + + const aliceReadReply = await dwn.processMessage(aggregator.did, aliceRead.message); + expect(aliceReadReply.status.code).to.equal(200, 'alice read'); + expect(aliceReadReply.entries?.length).to.equal(3, 'alice read records'); + expect(aliceReadReply.entries![0].recordId).to.equal(aliceNoteToCarol.message.recordId, 'alice note to carol public'); + expect(aliceReadReply.entries![1].recordId).to.equal(carolNoteToBob.message.recordId, 'carol note to bob public'); + expect(aliceReadReply.entries![2].recordId).to.equal(aliceNoteToCarolPrivate.message.recordId, 'alice to carol private'); + }); +}); diff --git a/tests/scenarios/subscriptions.spec.ts b/tests/scenarios/subscriptions.spec.ts index 580e789e9..a0030129b 100644 --- a/tests/scenarios/subscriptions.spec.ts +++ b/tests/scenarios/subscriptions.spec.ts @@ -680,71 +680,276 @@ export function testSubscriptionScenarios(): void { it('allows authorized subscriptions to records intended for a recipient', async () => { const alice = await TestDataGenerator.generateDidKeyPersona(); + + // alice installs a freeForAll protocol + const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : { ...freeForAll } + }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); + expect(protocolConfigureReply.status.code).to.equal(202); + const bob = await TestDataGenerator.generateDidKeyPersona(); const carol = await TestDataGenerator.generateDidKeyPersona(); - // bob subscribes to any messages he's authorized to see - const bobMessages:string[] = []; - const bobSubscribeHandler = async (event: MessageEvent):Promise => { + // bob subscribes to all records he's authorized to see, with alice as the recipient + const bobSubscribeAlice:string[] = []; + const bobSubscribeHandler = async (event: RecordEvent):Promise => { const { message } = event; - bobMessages.push(await Message.getCid(message)); + bobSubscribeAlice.push(await Message.getCid(message)); }; - const bobSubscribe = await TestDataGenerator.generateRecordsSubscribe({ + const bobSubscribeToAlice = await TestDataGenerator.generateRecordsSubscribe({ author : bob, - filter : { schema: 'http://schema1' } + filter : { protocol: freeForAll.protocol, recipient: alice.did } }); - const bobSubscribeReply = await dwn.processMessage(alice.did, bobSubscribe.message, { + const bobSubscribeReply = await dwn.processMessage(alice.did, bobSubscribeToAlice.message, { subscriptionHandler: bobSubscribeHandler }); expect(bobSubscribeReply.status.code).to.equal(200); expect(bobSubscribeReply.subscription).to.exist; - // carol subscribes to any messages she's the recipient of. - const carolMessages:string[] = []; + // carol subscribes to any messages that she or alice are the recipients of + const carolSubscribeCarolAndAlice:string[] = []; const carolSubscribeHandler = async (event: RecordEvent):Promise => { const { message } = event; - carolMessages.push(await Message.getCid(message)); + carolSubscribeCarolAndAlice.push(await Message.getCid(message)); }; - const carolSubscribe = await TestDataGenerator.generateRecordsSubscribe({ + const carolSubscribeToCarolAndAlice = await TestDataGenerator.generateRecordsSubscribe({ author : carol, - filter : { schema: 'http://schema1', recipient: carol.did } + filter : { protocol: freeForAll.protocol, recipient: [ alice.did, carol.did ] } }); - const carolSubscribeReply = await dwn.processMessage(alice.did, carolSubscribe.message, { + const carolSubscribeReply = await dwn.processMessage(alice.did, carolSubscribeToCarolAndAlice.message, { subscriptionHandler: carolSubscribeHandler }); expect(carolSubscribeReply.status.code).to.equal(200); expect(carolSubscribeReply.subscription).to.exist; - // write two messages for bob - const write1 = await TestDataGenerator.generateRecordsWrite({ author: alice, schema: 'http://schema1', recipient: bob.did }); - const write1Reply = await dwn.processMessage(alice.did, write1.message, { dataStream: write1.dataStream }); - expect(write1Reply.status.code).to.equal(202); + const recordParams = { + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }; - const write2 = await TestDataGenerator.generateRecordsWrite({ author: alice, schema: 'http://schema1', recipient: bob.did }); - const write2Reply = await dwn.processMessage(alice.did, write2.message, { dataStream: write2.dataStream }); - expect(write2Reply.status.code).to.equal(202); + // write a private and public message for alice from bob + const publicBobToAlice = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : bob, + recipient : alice.did, + published : true + }); + const publicBobToAliceReply = await dwn.processMessage(alice.did, publicBobToAlice.message, { dataStream: publicBobToAlice.dataStream }); + expect(publicBobToAliceReply.status.code).to.equal(202); + + const privateBobToAlice = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : bob, + recipient : alice.did, + published : false + }); + const privateBobToAliceReply = await dwn.processMessage(alice.did, privateBobToAlice.message, { dataStream: privateBobToAlice.dataStream }); + expect(privateBobToAliceReply.status.code).to.equal(202); + + // write a private message for alice from carol + const privateCarolToAlice = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : carol, + recipient : alice.did, + published : false + }); + const privateCarolToAliceReply = await dwn.processMessage(alice.did, privateCarolToAlice.message, { + dataStream: privateCarolToAlice.dataStream + }); + expect(privateCarolToAliceReply.status.code).to.equal(202); + + // write a public and private message from bob to carol + const publicBobToCarol = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : bob, + recipient : carol.did, + published : true + }); + const publicBobToCarolReply = await dwn.processMessage(alice.did, publicBobToCarol.message, { + dataStream: publicBobToCarol.dataStream + }); + expect(publicBobToCarolReply.status.code).to.equal(202); - // write one message for carol - const writeForCarol = await TestDataGenerator.generateRecordsWrite({ author: alice, schema: 'http://schema1', recipient: carol.did }); - const writeForCarolReply = await dwn.processMessage(alice.did, writeForCarol.message, { dataStream: writeForCarol.dataStream }); - expect(writeForCarolReply.status.code).to.equal(202); + const privateBobToCarol = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : bob, + recipient : carol.did, + published : false + }); + const privateBobToCarolReply = await dwn.processMessage(alice.did, privateBobToCarol.message, { + dataStream: privateBobToCarol.dataStream + }); + expect(privateBobToCarolReply.status.code).to.equal(202); await Poller.pollUntilSuccessOrTimeout(async () => { + // carol should have received the message intended for her + expect(carolSubscribeCarolAndAlice.length).to.equal(4); + expect(carolSubscribeCarolAndAlice).to.have.members([ + await Message.getCid(publicBobToAlice.message), + await Message.getCid(privateCarolToAlice.message), + await Message.getCid(publicBobToCarol.message), + await Message.getCid(privateBobToCarol.message), + ]); + // bob should have received the two messages intended for him - expect(bobMessages.length).to.equal(2); - expect(bobMessages).to.have.members([ - await Message.getCid(write1.message), - await Message.getCid(write2.message), + expect(bobSubscribeAlice.length).to.equal(2); + expect(bobSubscribeAlice).to.have.members([ + await Message.getCid(privateBobToAlice.message), + await Message.getCid(publicBobToAlice.message), ]); + }); + }); + + it('allows for authorized subscriptions to records authored by an author(s)', async () => { + const alice = await TestDataGenerator.generateDidKeyPersona(); + + // alice installs a freeForAll protocol + const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : { ...freeForAll } + }); + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); + expect(protocolConfigureReply.status.code).to.equal(202); + + const bob = await TestDataGenerator.generateDidKeyPersona(); + const carol = await TestDataGenerator.generateDidKeyPersona(); + + // bob subscribes to all records he's authorized to see, with alice as the author + const bobSubscribeAlice:string[] = []; + const bobSubscribeHandler = async (event: RecordEvent):Promise => { + const { message } = event; + bobSubscribeAlice.push(await Message.getCid(message)); + }; + const bobSubscribeToAlice = await TestDataGenerator.generateRecordsSubscribe({ + author : bob, + filter : { protocol: freeForAll.protocol, author: alice.did } + }); + + const bobSubscribeReply = await dwn.processMessage(alice.did, bobSubscribeToAlice.message, { + subscriptionHandler: bobSubscribeHandler + }); + expect(bobSubscribeReply.status.code).to.equal(200); + expect(bobSubscribeReply.subscription).to.exist; + + // carol subscribes to any messages that she or alice are the authors of + const carolSubscribeCarolAndAlice:string[] = []; + const carolSubscribeHandler = async (event: RecordEvent):Promise => { + const { message } = event; + carolSubscribeCarolAndAlice.push(await Message.getCid(message)); + }; + + const carolSubscribeToCarolAndAlice = await TestDataGenerator.generateRecordsSubscribe({ + author : carol, + filter : { protocol: freeForAll.protocol, author: [ alice.did, carol.did ] } + }); + + const carolSubscribeReply = await dwn.processMessage(alice.did, carolSubscribeToCarolAndAlice.message, { + subscriptionHandler: carolSubscribeHandler + }); + expect(carolSubscribeReply.status.code).to.equal(200); + expect(carolSubscribeReply.subscription).to.exist; + + const recordParams = { + protocol : freeForAll.protocol, + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }; + + //control: write a public message to bob (will not show up) + const publicAliceToBob = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : alice, + recipient : bob.did, + published : true + }); + const publicAliceToBobReply = await dwn.processMessage(alice.did, publicAliceToBob.message, { + dataStream: publicAliceToBob.dataStream + }); + expect(publicAliceToBobReply.status.code).to.equal(202); + + // write a private and public message from alice to carol + const publicAliceToCarol = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : alice, + recipient : carol.did, + published : true + }); + const publicAliceToCarolReply = await dwn.processMessage(alice.did, publicAliceToCarol.message, { + dataStream: publicAliceToCarol.dataStream + }); + expect(publicAliceToCarolReply.status.code).to.equal(202); + + const privateAliceToCarol = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : alice, + recipient : carol.did, + published : false + }); + const privateAliceToCarolReply = await dwn.processMessage(alice.did, privateAliceToCarol.message, { + dataStream: privateAliceToCarol.dataStream + }); + expect(privateAliceToCarolReply.status.code).to.equal(202); + + // write a private message for alice from carol + const privateCarolToAlice = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : carol, + recipient : alice.did, + published : false + }); + const privateCarolToAliceReply = await dwn.processMessage(alice.did, privateCarolToAlice.message, { + dataStream: privateCarolToAlice.dataStream + }); + expect(privateCarolToAliceReply.status.code).to.equal(202); + + // write a public and private message from bob to carol + const publicBobToCarol = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : bob, + recipient : carol.did, + published : true + }); + const publicBobToCarolReply = await dwn.processMessage(alice.did, publicBobToCarol.message, { + dataStream: publicBobToCarol.dataStream + }); + expect(publicBobToCarolReply.status.code).to.equal(202); + + const privateBobToCarol = await TestDataGenerator.generateRecordsWrite({ + ...recordParams, + author : bob, + recipient : carol.did, + published : false + }); + const privateBobToCarolReply = await dwn.processMessage(alice.did, privateBobToCarol.message, { + dataStream: privateBobToCarol.dataStream + }); + expect(privateBobToCarolReply.status.code).to.equal(202); + + await Poller.pollUntilSuccessOrTimeout(async () => { // carol should have received the message intended for her - expect(carolMessages.length).to.equal(1); - expect(carolMessages).to.have.members([ - await Message.getCid(writeForCarol.message), + expect(carolSubscribeCarolAndAlice.length).to.equal(4); + expect(carolSubscribeCarolAndAlice).to.have.members([ + await Message.getCid(publicAliceToCarol.message), + await Message.getCid(privateAliceToCarol.message), + await Message.getCid(publicAliceToBob.message), + await Message.getCid(privateCarolToAlice.message), + ]); + + // bob should have received the two messages intended for him + expect(bobSubscribeAlice.length).to.equal(2); + expect(bobSubscribeAlice).to.have.members([ + await Message.getCid(publicAliceToBob.message), + await Message.getCid(publicAliceToCarol.message) ]); }); });