From 92205ac194b75d1ead6cad8f6a33cc4b51202d2f Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 1 Jul 2024 19:16:07 -0400 Subject: [PATCH 1/8] modify records query/subscribe to accept array for author and/or recipient --- json-schemas/interface-methods/records-filter.json | 10 ++++++++-- src/handlers/records-query.ts | 2 +- src/handlers/records-subscribe.ts | 2 +- src/types/records-types.ts | 4 ++-- tests/handlers/records-query.spec.ts | 8 ++++---- tests/handlers/records-read.spec.ts | 2 +- tests/scenarios/end-to-end-tests.spec.ts | 2 +- tests/scenarios/subscriptions.spec.ts | 2 +- 8 files changed, 19 insertions(+), 13 deletions(-) diff --git a/json-schemas/interface-methods/records-filter.json b/json-schemas/interface-methods/records-filter.json index 2c1107cfa..a2f51e706 100644 --- a/json-schemas/interface-methods/records-filter.json +++ b/json-schemas/interface-methods/records-filter.json @@ -12,13 +12,19 @@ "type": "string" }, "author": { - "$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" + "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..d45cc8a04 100644 --- a/src/handlers/records-query.ts +++ b/src/handlers/records-query.ts @@ -154,7 +154,7 @@ export class RecordsQueryHandler implements MethodHandler { filters.push(RecordsQueryHandler.buildUnpublishedRecordsByQueryAuthorFilter(recordsQuery)); const recipientFilter = recordsQuery.message.descriptor.filter.recipient; - if (recipientFilter === undefined || recipientFilter === recordsQuery.author) { + if (recipientFilter === undefined || recipientFilter.length === 1 && recipientFilter.includes(recordsQuery.author!)) { filters.push(RecordsQueryHandler.buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery)); } diff --git a/src/handlers/records-subscribe.ts b/src/handlers/records-subscribe.ts index ea1416ae5..336c2400a 100644 --- a/src/handlers/records-subscribe.ts +++ b/src/handlers/records-subscribe.ts @@ -130,7 +130,7 @@ export class RecordsSubscribeHandler implements MethodHandler { filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsBySubscribeAuthorFilter(recordsSubscribe)); const recipientFilter = recordsSubscribe.message.descriptor.filter.recipient; - if (recipientFilter === undefined || recipientFilter === recordsSubscribe.author) { + if (recipientFilter === undefined || recipientFilter.length === 1 && recipientFilter.includes(recordsSubscribe.author!)) { filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsForSubscribeAuthorFilter(recordsSubscribe)); } diff --git a/src/types/records-types.ts b/src/types/records-types.ts index 4d94d0fd7..e097153ad 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[]; attester?: string; - recipient?: string; + recipient?: string[]; protocol?: string; protocolPath?: string; published?: boolean; diff --git a/tests/handlers/records-query.spec.ts b/tests/handlers/records-query.spec.ts index d528114bf..ee29d2ec8 100644 --- a/tests/handlers/records-query.spec.ts +++ b/tests/handlers/records-query.spec.ts @@ -305,7 +305,7 @@ export function testRecordsQueryHandler(): void { recordsQuery = await TestDataGenerator.generateRecordsQuery({ author : alice, filter : { - author : bob.did, + author : [ bob.did ], protocol : protocolDefinition.protocol, schema : protocolDefinition.types.post.schema, dataFormat : protocolDefinition.types.post.dataFormats[0], @@ -1566,7 +1566,7 @@ export function testRecordsQueryHandler(): void { // filter for public records with carol as recipient const bobQueryCarolMessageData = await TestDataGenerator.generateRecordsQuery({ author : bob, - filter : { schema, recipient: carol.did } + filter : { schema, recipient: [ carol.did ] } }); const replyToBobCarolQuery = await dwn.processMessage(alice.did, bobQueryCarolMessageData.message); expect(replyToBobCarolQuery.status.code).to.equal(200); @@ -1576,7 +1576,7 @@ export function testRecordsQueryHandler(): void { // filter for explicit unpublished public records with carol as recipient, should not return any. const bobQueryCarolMessageDataUnpublished = await TestDataGenerator.generateRecordsQuery({ author : bob, - filter : { schema, recipient: carol.did, published: false } + filter : { schema, recipient: [ carol.did ], published: false } }); const replyToBobCarolUnpublishedQuery = await dwn.processMessage(alice.did, bobQueryCarolMessageDataUnpublished.message); expect(replyToBobCarolUnpublishedQuery.status.code).to.equal(200); @@ -1759,7 +1759,7 @@ export function testRecordsQueryHandler(): void { const bobQueryMessageData = await TestDataGenerator.generateRecordsQuery({ author : alice, - filter : { recipient: bob.did } // alice as the DWN owner querying bob's records + filter : { recipient: [ bob.did ] } // alice as the DWN owner querying bob's records }); const replyToBobQuery = await dwn.processMessage(alice.did, bobQueryMessageData.message); diff --git a/tests/handlers/records-read.spec.ts b/tests/handlers/records-read.spec.ts index 7bddd5f98..c384a74c7 100644 --- a/tests/handlers/records-read.spec.ts +++ b/tests/handlers/records-read.spec.ts @@ -690,7 +690,7 @@ export function testRecordsReadHandler(): void { signer : Jws.createSigner(bob), filter : { protocolPath : 'thread/participant', - recipient : bob.did, + recipient : [ bob.did ], contextId : threadRecord.message.contextId }, }); diff --git a/tests/scenarios/end-to-end-tests.spec.ts b/tests/scenarios/end-to-end-tests.spec.ts index d4caa2c84..abc874ed9 100644 --- a/tests/scenarios/end-to-end-tests.spec.ts +++ b/tests/scenarios/end-to-end-tests.spec.ts @@ -183,7 +183,7 @@ export function testEndToEndScenarios(): void { signer : Jws.createSigner(bob), filter : { protocolPath : 'thread/participant', - recipient : bob.did, + recipient : [ bob.did ], contextId : threadRecord.message.contextId }, }); diff --git a/tests/scenarios/subscriptions.spec.ts b/tests/scenarios/subscriptions.spec.ts index 580e789e9..0458f202a 100644 --- a/tests/scenarios/subscriptions.spec.ts +++ b/tests/scenarios/subscriptions.spec.ts @@ -710,7 +710,7 @@ export function testSubscriptionScenarios(): void { const carolSubscribe = await TestDataGenerator.generateRecordsSubscribe({ author : carol, - filter : { schema: 'http://schema1', recipient: carol.did } + filter : { schema: 'http://schema1', recipient: [ carol.did ] } }); const carolSubscribeReply = await dwn.processMessage(alice.did, carolSubscribe.message, { From 27b0795b32016bb0cd640229da2458b61330a620 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 15 Aug 2024 15:16:43 -0400 Subject: [PATCH 2/8] handle multiple recipient and author record queries --- .../interface-methods/records-filter.json | 20 +- src/handlers/records-query.ts | 9 +- src/handlers/records-subscribe.ts | 7 +- src/types/records-types.ts | 4 +- src/utils/records.ts | 16 + tests/scenarios/aggregator.spec.ts | 417 ++++++++++++++++++ 6 files changed, 455 insertions(+), 18 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 a2f51e706..0e9d0e1d4 100644 --- a/json-schemas/interface-methods/records-filter.json +++ b/json-schemas/interface-methods/records-filter.json @@ -12,19 +12,27 @@ "type": "string" }, "author": { - "type": "array", - "items": { + "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": { - "type": "array", - "items": { + "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 d45cc8a04..84a391ffa 100644 --- a/src/handlers/records-query.ts +++ b/src/handlers/records-query.ts @@ -151,15 +151,14 @@ 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.length === 1 && recipientFilter.includes(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)); + } else if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsQuery.author!)) { + filters.push(RecordsQueryHandler.buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery)); } } diff --git a/src/handlers/records-subscribe.ts b/src/handlers/records-subscribe.ts index 336c2400a..081709ae6 100644 --- a/src/handlers/records-subscribe.ts +++ b/src/handlers/records-subscribe.ts @@ -129,13 +129,10 @@ 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.length === 1 && recipientFilter.includes(recordsSubscribe.author!)) { - filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsForSubscribeAuthorFilter(recordsSubscribe)); - } - if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload!)) { filters.push(RecordsSubscribeHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsSubscribe)); + } else 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 e097153ad..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..73092f018 100644 --- a/src/utils/records.ts +++ b/src/utils/records.ts @@ -531,4 +531,20 @@ export class Records { return true; } + + 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; + } + + 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/scenarios/aggregator.spec.ts b/tests/scenarios/aggregator.spec.ts new file mode 100644 index 000000000..ba1c91f86 --- /dev/null +++ b/tests/scenarios/aggregator.spec.ts @@ -0,0 +1,417 @@ +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); + +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 aggregator protocol that allows members of the aggregator to write notes to the aggregator and allow anyone to read or query + 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 aggregation of records from multiple authors', 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', + 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 crol 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'); + }); +}); From 8c85752e1e3da802e95507bc9c0c35e98dd25e6f Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 15 Aug 2024 16:27:16 -0400 Subject: [PATCH 3/8] handle multiple recipient and author record queries --- src/handlers/records-query.ts | 4 +- src/handlers/records-subscribe.ts | 8 +- src/utils/records.ts | 10 + tests/handlers/records-query.spec.ts | 283 ++++++++++++++++++++++- tests/handlers/records-read.spec.ts | 2 +- tests/scenarios/aggregator.spec.ts | 23 +- tests/scenarios/end-to-end-tests.spec.ts | 2 +- tests/scenarios/subscriptions.spec.ts | 2 +- 8 files changed, 320 insertions(+), 14 deletions(-) diff --git a/src/handlers/records-query.ts b/src/handlers/records-query.ts index 84a391ffa..b90754a65 100644 --- a/src/handlers/records-query.ts +++ b/src/handlers/records-query.ts @@ -157,7 +157,9 @@ export class RecordsQueryHandler implements MethodHandler { if (Records.shouldProtocolAuthorize(recordsQuery.signaturePayload!)) { filters.push(RecordsQueryHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsQuery)); - } else if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsQuery.author!)) { + } + + if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsQuery.author!)) { filters.push(RecordsQueryHandler.buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery)); } } diff --git a/src/handlers/records-subscribe.ts b/src/handlers/records-subscribe.ts index 081709ae6..850e7a6c1 100644 --- a/src/handlers/records-subscribe.ts +++ b/src/handlers/records-subscribe.ts @@ -127,11 +127,15 @@ export class RecordsSubscribeHandler implements MethodHandler { } if (Records.filterIncludesUnpublishedRecords(filter)) { - filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsBySubscribeAuthorFilter(recordsSubscribe)); + if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsSubscribe.author!)) { + filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsBySubscribeAuthorFilter(recordsSubscribe)); + } if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload!)) { filters.push(RecordsSubscribeHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsSubscribe)); - } else if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author!)) { + } + + if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author!)) { filters.push(RecordsSubscribeHandler.buildUnpublishedRecordsForSubscribeAuthorFilter(recordsSubscribe)); } } diff --git a/src/utils/records.ts b/src/utils/records.ts index 73092f018..62a9d8ec4 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; } diff --git a/tests/handlers/records-query.spec.ts b/tests/handlers/records-query.spec.ts index ee29d2ec8..8eb094ad8 100644 --- a/tests/handlers/records-query.spec.ts +++ b/tests/handlers/records-query.spec.ts @@ -305,7 +305,7 @@ export function testRecordsQueryHandler(): void { recordsQuery = await TestDataGenerator.generateRecordsQuery({ author : alice, filter : { - author : [ bob.did ], + author : bob.did, protocol : protocolDefinition.protocol, schema : protocolDefinition.types.post.schema, dataFormat : protocolDefinition.types.post.dataFormats[0], @@ -318,6 +318,281 @@ export function testRecordsQueryHandler(): void { expect(queryReply.entries![0].recordId).to.equal(bobAuthorWrite.message.recordId); }); + it('should be able to query by multiple authors', async () => { + // scenario: alice, bob and carol author records into alice's DWN. + // alice is able to query based on multiple authors. + + 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 aliceAuthorWrite = await TestDataGenerator.generateRecordsWrite({ + author : alice, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const aliceAuthorReply = await dwn.processMessage(alice.did, aliceAuthorWrite.message, { dataStream: aliceAuthorWrite.dataStream }); + expect(aliceAuthorReply.status.code).to.equal(202); + + const bobAuthorWrite = await TestDataGenerator.generateRecordsWrite({ + author : bob, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const bobAuthorReply = await dwn.processMessage(alice.did, bobAuthorWrite.message, { dataStream: bobAuthorWrite.dataStream }); + expect(bobAuthorReply.status.code).to.equal(202); + + const carolAuthorWrite = await TestDataGenerator.generateRecordsWrite({ + author : carol, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const carolAuthorReply = await dwn.processMessage(alice.did, carolAuthorWrite.message, { dataStream: carolAuthorWrite.dataStream }); + expect(carolAuthorReply.status.code).to.equal(202); + + // alice queries with an empty array, gets all + let recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post', + author : [] + } + }); + let queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(3); + expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ + aliceAuthorWrite.message.recordId, + bobAuthorWrite.message.recordId, + carolAuthorWrite.message.recordId + ]); + + // filter for alice and bob as authors + 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); + expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ + aliceAuthorWrite.message.recordId, + bobAuthorWrite.message.recordId + ]); + }); + + 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); + }); + + it('should be able to query by multiple recipients', async () => { + // scenario: alice, bob and carol author records for various recipients into alice's DWN. + // alice is able to filter based on multiple recipients + + 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 bobToAliceWrite = await TestDataGenerator.generateRecordsWrite({ + author : bob, + recipient : alice.did, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const bobToAliceReply = await dwn.processMessage(alice.did, bobToAliceWrite.message, { dataStream: bobToAliceWrite.dataStream }); + expect(bobToAliceReply.status.code).to.equal(202); + + const aliceToBobWrite = 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, aliceToBobWrite.message, { dataStream: aliceToBobWrite.dataStream }); + expect(aliceToBobReply.status.code).to.equal(202); + + const carolToBobWrite = await TestDataGenerator.generateRecordsWrite({ + author : carol, + recipient : bob.did, + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post' + }); + const carolToBobReply = await dwn.processMessage(alice.did, carolToBobWrite.message, { dataStream: carolToBobWrite.dataStream }); + expect(carolToBobReply.status.code).to.equal(202); + + const aliceToCarolWrite = 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, aliceToCarolWrite.message, { dataStream: aliceToCarolWrite.dataStream }); + expect(aliceToCarolReply.status.code).to.equal(202); + + // alice queries with an empty array, gets all + let recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + protocolPath : 'post', + recipient : [] + } + }); + let queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + expect(queryReply.status.code).to.equal(200); + expect(queryReply.entries?.length).to.equal(4); + expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ + bobToAliceWrite.message.recordId, + aliceToBobWrite.message.recordId, + carolToBobWrite.message.recordId, + aliceToCarolWrite.message.recordId + ]); + + // filter for alice and bob as authors + recordsQuery = await TestDataGenerator.generateRecordsQuery({ + author : alice, + filter : { + recipient : [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(3); + expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ + bobToAliceWrite.message.recordId, + aliceToBobWrite.message.recordId, + carolToBobWrite.message.recordId + ]); + }); + it('should be able to query for published records', async () => { const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); @@ -1566,7 +1841,7 @@ export function testRecordsQueryHandler(): void { // filter for public records with carol as recipient const bobQueryCarolMessageData = await TestDataGenerator.generateRecordsQuery({ author : bob, - filter : { schema, recipient: [ carol.did ] } + filter : { schema, recipient: carol.did } }); const replyToBobCarolQuery = await dwn.processMessage(alice.did, bobQueryCarolMessageData.message); expect(replyToBobCarolQuery.status.code).to.equal(200); @@ -1576,7 +1851,7 @@ export function testRecordsQueryHandler(): void { // filter for explicit unpublished public records with carol as recipient, should not return any. const bobQueryCarolMessageDataUnpublished = await TestDataGenerator.generateRecordsQuery({ author : bob, - filter : { schema, recipient: [ carol.did ], published: false } + filter : { schema, recipient: carol.did, published: false } }); const replyToBobCarolUnpublishedQuery = await dwn.processMessage(alice.did, bobQueryCarolMessageDataUnpublished.message); expect(replyToBobCarolUnpublishedQuery.status.code).to.equal(200); @@ -1759,7 +2034,7 @@ export function testRecordsQueryHandler(): void { const bobQueryMessageData = await TestDataGenerator.generateRecordsQuery({ author : alice, - filter : { recipient: [ bob.did ] } // alice as the DWN owner querying bob's records + filter : { recipient: bob.did } // alice as the DWN owner querying bob's records }); const replyToBobQuery = await dwn.processMessage(alice.did, bobQueryMessageData.message); diff --git a/tests/handlers/records-read.spec.ts b/tests/handlers/records-read.spec.ts index c384a74c7..7bddd5f98 100644 --- a/tests/handlers/records-read.spec.ts +++ b/tests/handlers/records-read.spec.ts @@ -690,7 +690,7 @@ export function testRecordsReadHandler(): void { signer : Jws.createSigner(bob), filter : { protocolPath : 'thread/participant', - recipient : [ bob.did ], + recipient : bob.did, contextId : threadRecord.message.contextId }, }); diff --git a/tests/scenarios/aggregator.spec.ts b/tests/scenarios/aggregator.spec.ts index ba1c91f86..bb65f31b0 100644 --- a/tests/scenarios/aggregator.spec.ts +++ b/tests/scenarios/aggregator.spec.ts @@ -18,6 +18,11 @@ 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; @@ -44,7 +49,8 @@ describe('Aggregator Model', () => { } }; - // A simple aggregator protocol that allows members of the aggregator to write notes to the aggregator and allow anyone to read or query + // 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, @@ -100,7 +106,17 @@ describe('Aggregator Model', () => { await dwn.close(); }); - it('should support aggregation of records from multiple authors', async () => { + 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({ @@ -144,7 +160,6 @@ describe('Aggregator Model', () => { expect(danielProtocolReply.status.code).to.equal(202, 'daniel configure'); - // The aggregator creates member records for alice, bob and carol const aliceMemberData = TestDataGenerator.randomBytes(256); @@ -292,7 +307,7 @@ describe('Aggregator Model', () => { 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 crol from alice and bob + // create private notes to carol from alice and bob const alicePrivateNoteToCarol = TestDataGenerator.randomBytes(256); const aliceNoteToCarolPrivate = await RecordsWrite.create({ signer : Jws.createSigner(alice), diff --git a/tests/scenarios/end-to-end-tests.spec.ts b/tests/scenarios/end-to-end-tests.spec.ts index abc874ed9..d4caa2c84 100644 --- a/tests/scenarios/end-to-end-tests.spec.ts +++ b/tests/scenarios/end-to-end-tests.spec.ts @@ -183,7 +183,7 @@ export function testEndToEndScenarios(): void { signer : Jws.createSigner(bob), filter : { protocolPath : 'thread/participant', - recipient : [ bob.did ], + recipient : bob.did, contextId : threadRecord.message.contextId }, }); diff --git a/tests/scenarios/subscriptions.spec.ts b/tests/scenarios/subscriptions.spec.ts index 0458f202a..580e789e9 100644 --- a/tests/scenarios/subscriptions.spec.ts +++ b/tests/scenarios/subscriptions.spec.ts @@ -710,7 +710,7 @@ export function testSubscriptionScenarios(): void { const carolSubscribe = await TestDataGenerator.generateRecordsSubscribe({ author : carol, - filter : { schema: 'http://schema1', recipient: [ carol.did ] } + filter : { schema: 'http://schema1', recipient: carol.did } }); const carolSubscribeReply = await dwn.processMessage(alice.did, carolSubscribe.message, { From 07890392f7f04dbdf2e6f5b56d97e4c9b9334144 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 15 Aug 2024 20:16:37 -0400 Subject: [PATCH 4/8] scenario test for recipients --- tests/scenarios/aggregator.spec.ts | 315 +++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) diff --git a/tests/scenarios/aggregator.spec.ts b/tests/scenarios/aggregator.spec.ts index bb65f31b0..dd030c5c8 100644 --- a/tests/scenarios/aggregator.spec.ts +++ b/tests/scenarios/aggregator.spec.ts @@ -429,4 +429,319 @@ describe('Aggregator Model', () => { 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'); + }); }); From 643bb25b0f6aabd68f6ffc19fed249a099a40aab Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 16 Aug 2024 10:20:34 -0400 Subject: [PATCH 5/8] increase test coverage with a more thourough test case --- tests/handlers/records-query.spec.ts | 412 +++++++++++++++------------ 1 file changed, 235 insertions(+), 177 deletions(-) diff --git a/tests/handlers/records-query.spec.ts b/tests/handlers/records-query.spec.ts index 8eb094ad8..bf2356c9d 100644 --- a/tests/handlers/records-query.spec.ts +++ b/tests/handlers/records-query.spec.ts @@ -318,93 +318,6 @@ export function testRecordsQueryHandler(): void { expect(queryReply.entries![0].recordId).to.equal(bobAuthorWrite.message.recordId); }); - it('should be able to query by multiple authors', async () => { - // scenario: alice, bob and carol author records into alice's DWN. - // alice is able to query based on multiple authors. - - 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 aliceAuthorWrite = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocolDefinition.protocol, - schema : protocolDefinition.types.post.schema, - dataFormat : protocolDefinition.types.post.dataFormats[0], - protocolPath : 'post' - }); - const aliceAuthorReply = await dwn.processMessage(alice.did, aliceAuthorWrite.message, { dataStream: aliceAuthorWrite.dataStream }); - expect(aliceAuthorReply.status.code).to.equal(202); - - const bobAuthorWrite = await TestDataGenerator.generateRecordsWrite({ - author : bob, - protocol : protocolDefinition.protocol, - schema : protocolDefinition.types.post.schema, - dataFormat : protocolDefinition.types.post.dataFormats[0], - protocolPath : 'post' - }); - const bobAuthorReply = await dwn.processMessage(alice.did, bobAuthorWrite.message, { dataStream: bobAuthorWrite.dataStream }); - expect(bobAuthorReply.status.code).to.equal(202); - - const carolAuthorWrite = await TestDataGenerator.generateRecordsWrite({ - author : carol, - protocol : protocolDefinition.protocol, - schema : protocolDefinition.types.post.schema, - dataFormat : protocolDefinition.types.post.dataFormats[0], - protocolPath : 'post' - }); - const carolAuthorReply = await dwn.processMessage(alice.did, carolAuthorWrite.message, { dataStream: carolAuthorWrite.dataStream }); - expect(carolAuthorReply.status.code).to.equal(202); - - // alice queries with an empty array, gets all - let recordsQuery = await TestDataGenerator.generateRecordsQuery({ - author : alice, - filter : { - protocol : protocolDefinition.protocol, - schema : protocolDefinition.types.post.schema, - dataFormat : protocolDefinition.types.post.dataFormats[0], - protocolPath : 'post', - author : [] - } - }); - let queryReply = await dwn.processMessage(alice.did, recordsQuery.message); - expect(queryReply.status.code).to.equal(200); - expect(queryReply.entries?.length).to.equal(3); - expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ - aliceAuthorWrite.message.recordId, - bobAuthorWrite.message.recordId, - carolAuthorWrite.message.recordId - ]); - - // filter for alice and bob as authors - 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); - expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ - aliceAuthorWrite.message.recordId, - bobAuthorWrite.message.recordId - ]); - }); - 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. @@ -1741,121 +1654,266 @@ 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 is 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 } - ); - - // 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); + // 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 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); + // 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 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 alicePrivateToBobReply = await dwn.processMessage(alice.did, alicePrivateToBob.message, { dataStream: alicePrivateToBob.dataStream }); + expect(alicePrivateToBobReply.status.code).to.equal(202); - 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 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); - 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); + // 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 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); + 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); - // test correctness for Bob's query - const bobQueryMessageData = await TestDataGenerator.generateRecordsQuery({ - author : bob, - filter : { schema } + // 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 replyToBob = await dwn.processMessage(alice.did, bobQueryMessageData.message); + const bobPrivateToAliceReply = await dwn.processMessage(alice.did, bobPrivateToAlice.message, { + dataStream: bobPrivateToAlice.dataStream + }); + expect(bobPrivateToAliceReply.status.code).to.equal(202); - expect(replyToBob.status.code).to.equal(200); - expect(replyToBob.entries?.length).to.equal(4); // expect 4 records + 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 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); + // 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); - // check for explicitly published:false records for Bob - const bobQueryPublishedFalse = await TestDataGenerator.generateRecordsQuery({ - author : bob, - filter : { schema, published: false } + 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 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 ]); + const alicePublicToCarolReply = await dwn.processMessage(alice.did, alicePublicToCarol.message, { + dataStream: alicePublicToCarol.dataStream + }); + expect(alicePublicToCarolReply.status.code).to.equal(202); - // test correctness for Alice's query - const aliceQueryMessageData = await TestDataGenerator.generateRecordsQuery({ - author : alice, - filter : { schema } + // 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 replyToAliceQuery = await dwn.processMessage(alice.did, aliceQueryMessageData.message); + 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); - expect(replyToAliceQuery.status.code).to.equal(200); - expect(replyToAliceQuery.entries?.length).to.equal(5); // expect all 5 records + // 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); - // filter for public records with carol as recipient - const bobQueryCarolMessageData = await TestDataGenerator.generateRecordsQuery({ - author : bob, - filter : { schema, recipient: carol.did } + 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 }); - 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(carolPublicToBobReply.status.code).to.equal(202); - // filter for explicit unpublished public records with carol as recipient, should not return any. - const bobQueryCarolMessageDataUnpublished = await TestDataGenerator.generateRecordsQuery({ + // bob queries for records with himself and alice as recipients + 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', 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 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 paginate correctly for fetchRecordsAsNonOwner()', async () => { From 031c98e7f612c1aaf8653f37eba67cc1e6eea3fa Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 16 Aug 2024 10:38:48 -0400 Subject: [PATCH 6/8] clean up tests to remove some dupliation --- tests/handlers/records-query.spec.ts | 383 +++++++++++++++++++++------ 1 file changed, 299 insertions(+), 84 deletions(-) diff --git a/tests/handlers/records-query.spec.ts b/tests/handlers/records-query.spec.ts index bf2356c9d..a7bd14bfa 100644 --- a/tests/handlers/records-query.spec.ts +++ b/tests/handlers/records-query.spec.ts @@ -316,6 +316,36 @@ 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 () => { @@ -401,95 +431,27 @@ 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(aliceToCarol.message.recordId); - }); - - it('should be able to query by multiple recipients', async () => { - // scenario: alice, bob and carol author records for various recipients into alice's DWN. - // alice is able to filter based on multiple recipients - - 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 bobToAliceWrite = await TestDataGenerator.generateRecordsWrite({ - author : bob, - recipient : alice.did, - protocol : protocolDefinition.protocol, - schema : protocolDefinition.types.post.schema, - dataFormat : protocolDefinition.types.post.dataFormats[0], - protocolPath : 'post' - }); - const bobToAliceReply = await dwn.processMessage(alice.did, bobToAliceWrite.message, { dataStream: bobToAliceWrite.dataStream }); - expect(bobToAliceReply.status.code).to.equal(202); - - const aliceToBobWrite = 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, aliceToBobWrite.message, { dataStream: aliceToBobWrite.dataStream }); - expect(aliceToBobReply.status.code).to.equal(202); - - const carolToBobWrite = await TestDataGenerator.generateRecordsWrite({ - author : carol, - recipient : bob.did, - protocol : protocolDefinition.protocol, - schema : protocolDefinition.types.post.schema, - dataFormat : protocolDefinition.types.post.dataFormats[0], - protocolPath : 'post' - }); - const carolToBobReply = await dwn.processMessage(alice.did, carolToBobWrite.message, { dataStream: carolToBobWrite.dataStream }); - expect(carolToBobReply.status.code).to.equal(202); - - const aliceToCarolWrite = 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, aliceToCarolWrite.message, { dataStream: aliceToCarolWrite.dataStream }); - expect(aliceToCarolReply.status.code).to.equal(202); - - // alice queries with an empty array, gets all - let recordsQuery = await TestDataGenerator.generateRecordsQuery({ + // 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', - recipient : [] + protocolPath : 'post' } }); - let queryReply = await dwn.processMessage(alice.did, recordsQuery.message); + queryReply = await dwn.processMessage(alice.did, recordsQuery.message); expect(queryReply.status.code).to.equal(200); - expect(queryReply.entries?.length).to.equal(4); - expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ - bobToAliceWrite.message.recordId, - aliceToBobWrite.message.recordId, - carolToBobWrite.message.recordId, - aliceToCarolWrite.message.recordId - ]); + expect(queryReply.entries?.length).to.equal(2); - // filter for alice and bob as authors + // query for both recipients explicitly recordsQuery = await TestDataGenerator.generateRecordsQuery({ author : alice, filter : { - recipient : [alice.did, bob.did], + recipient : [bob.did, carol.did], protocol : protocolDefinition.protocol, schema : protocolDefinition.types.post.schema, dataFormat : protocolDefinition.types.post.dataFormats[0], @@ -498,12 +460,7 @@ export function testRecordsQueryHandler(): void { }); queryReply = await dwn.processMessage(alice.did, recordsQuery.message); expect(queryReply.status.code).to.equal(200); - expect(queryReply.entries?.length).to.equal(3); - expect(queryReply.entries?.map(e => e.recordId)).to.have.members([ - bobToAliceWrite.message.recordId, - aliceToBobWrite.message.recordId, - carolToBobWrite.message.recordId - ]); + expect(queryReply.entries?.length).to.equal(2); }); it('should be able to query for published records', async () => { @@ -1654,7 +1611,7 @@ 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 specific recipient(s)', async () => { + 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 @@ -1908,7 +1865,7 @@ export function testRecordsQueryHandler(): void { 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 private messages she authored to alice, and her own private messages with herself as the recipient + // 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, @@ -1916,6 +1873,264 @@ export function testRecordsQueryHandler(): void { ]); }); + 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); + + // 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 alicePrivateToBobReply = await dwn.processMessage(alice.did, alicePrivateToBob.message, { dataStream: alicePrivateToBob.dataStream }); + expect(alicePrivateToBobReply.status.code).to.equal(202); + + 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 bobPrivateToAliceReply = await dwn.processMessage(alice.did, bobPrivateToAlice.message, { + dataStream: bobPrivateToAlice.dataStream + }); + expect(bobPrivateToAliceReply.status.code).to.equal(202); + + 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); + + // 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); + + 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 authors + const bobQueryMessagesForBobAlice = await TestDataGenerator.generateRecordsQuery({ + author : bob, + 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 () => { const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); From 38dc6103f10ce9d1ab3f29edc35e2662ffea4e91 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 16 Aug 2024 10:47:03 -0400 Subject: [PATCH 7/8] additional comments --- src/utils/records.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/utils/records.ts b/src/utils/records.ts index 62a9d8ec4..e6f33d643 100644 --- a/src/utils/records.ts +++ b/src/utils/records.ts @@ -542,6 +542,13 @@ 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; @@ -550,6 +557,13 @@ export class Records { 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; From c06f76ad189b01198db9f6ea8eb392a718430724 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 16 Aug 2024 16:51:32 -0400 Subject: [PATCH 8/8] subscription tests for multiple authore/recipients --- tests/scenarios/subscriptions.spec.ts | 267 +++++++++++++++++++++++--- 1 file changed, 236 insertions(+), 31 deletions(-) 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) ]); }); });