From 92b679249180c3eb9f4ca4ea32739950c98702f1 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 21 Jun 2024 12:55:14 -0400 Subject: [PATCH] EventsQuery by protocol (#762) This PR is to implement a `protocol` filter for the `EventsQuery` interface which accounts for receiving permission messages scoped to the filtered protocol via tag filters, as well as other messages directly indexed to the protocol. It also resolves this issue https://github.com/TBD54566975/dwn-sdk-js/issues/663, which simplifies the type of filters `EventsQuery` implements to reduce complexity of which messages may or may not be returned with the filter. Since `EventsQuery` is being used for sync/selective sync, the `protocol` filter is most important, if further filters are needed down the line, we will implement them according to scenarios put in place so we can make sure the correct messages arrive with the query results. The Additional filters I left are for `interface`, `method` and `messageTimestamp` which are compatible with all types of messages. NOTE: I cleaned up something I didn't like with `EventsQuery` from the prior PR instead of the message descriptor having an undefined filter, it is forced to have an empty filters array. NOTE2: `EventsSubscribe` does not yet handle filtering for protocols which include the permissions messages, that will be done in a separate PR. --- .../interface-methods/events-filter.json | 77 +- .../interface-methods/events-query.json | 4 +- src/handlers/events-query.ts | 6 +- src/interfaces/events-query.ts | 9 +- src/interfaces/events-subscribe.ts | 5 +- src/types/events-types.ts | 30 +- src/utils/events.ts | 78 +- src/utils/records.ts | 2 +- tests/handlers/events-query.spec.ts | 23 +- tests/interfaces/events-query.spec.ts | 45 +- tests/scenarios/events-query.spec.ts | 934 +++--------------- tests/scenarios/subscriptions.spec.ts | 631 +----------- tests/utils/events.spec.ts | 120 +++ 13 files changed, 347 insertions(+), 1617 deletions(-) create mode 100644 tests/utils/events.spec.ts diff --git a/json-schemas/interface-methods/events-filter.json b/json-schemas/interface-methods/events-filter.json index 2abc89126..79baeb889 100644 --- a/json-schemas/interface-methods/events-filter.json +++ b/json-schemas/interface-methods/events-filter.json @@ -23,31 +23,7 @@ "protocol": { "type": "string" }, - "protocolPath": { - "type": "string" - }, - "recipient": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" - }, - "contextId": { - "type": "string" - }, - "schema": { - "type": "string" - }, - "recordId": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "dataFormat": { - "type": "string" - }, - "dataSize": { - "$ref": "https://identity.foundation/dwn/json-schemas/number-range-filter.json" - }, - "dateCreated": { + "messageTimestamp": { "type": "object", "minProperties": 1, "additionalProperties": false, @@ -59,57 +35,6 @@ "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" } } - }, - "datePublished": { - "type": "object", - "minProperties": 1, - "additionalProperties": false, - "properties": { - "from": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" - }, - "to": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" - } - } - }, - "dateUpdated": { - "type": "object", - "minProperties": 1, - "additionalProperties": false, - "properties": { - "from": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" - }, - "to": { - "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" - } - } - } - }, - "dependencies": { - "datePublished": { - "oneOf": [ - { - "properties": { - "published": { - "enum": [ - true - ] - } - }, - "required": [ - "published" - ] - }, - { - "not": { - "required": [ - "published" - ] - } - } - ] } } } \ No newline at end of file diff --git a/json-schemas/interface-methods/events-query.json b/json-schemas/interface-methods/events-query.json index be00ac2c8..d31c2d01a 100644 --- a/json-schemas/interface-methods/events-query.json +++ b/json-schemas/interface-methods/events-query.json @@ -17,7 +17,8 @@ "required": [ "interface", "method", - "messageTimestamp" + "messageTimestamp", + "filters" ], "properties": { "interface": { @@ -37,7 +38,6 @@ }, "filters": { "type": "array", - "minItems": 1, "items": { "$ref": "https://identity.foundation/dwn/json-schemas/events-filter.json" } diff --git a/src/handlers/events-query.ts b/src/handlers/events-query.ts index 2b8485758..c011edb3b 100644 --- a/src/handlers/events-query.ts +++ b/src/handlers/events-query.ts @@ -32,10 +32,8 @@ export class EventsQueryHandler implements MethodHandler { return messageReplyFromError(e, 401); } - // if no filter is present in the the `EventsQuery` descriptor, we pass an empty array of filters to the `queryEvents` method - // this will return all events in the event log for the given tenant beyond the cursor provided. - // if no cursor is provided, it will return all events - const eventFilters = message.descriptor.filters ? Events.convertFilters(message.descriptor.filters) : []; + // an empty array of filters means no filtering and all events are returned + const eventFilters = Events.convertFilters(message.descriptor.filters); const { events, cursor } = await this.eventLog.queryEvents(tenant, eventFilters, message.descriptor.cursor); return { diff --git a/src/interfaces/events-query.ts b/src/interfaces/events-query.ts index df3f1afdd..1df3f7cc5 100644 --- a/src/interfaces/events-query.ts +++ b/src/interfaces/events-query.ts @@ -7,8 +7,8 @@ import { Events } from '../utils/events.js'; import { Message } from '../core/message.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { Time } from '../utils/time.js'; +import { validateProtocolUrlNormalized } from '../utils/url.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; -import { validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js'; export type EventsQueryOptions = { signer: Signer; @@ -23,13 +23,10 @@ export class EventsQuery extends AbstractMessage{ Message.validateJsonSchema(message); await Message.validateSignatureStructure(message.authorization.signature, message.descriptor); - for (const filter of message.descriptor.filters || []) { + for (const filter of message.descriptor.filters) { if ('protocol' in filter && filter.protocol !== undefined) { validateProtocolUrlNormalized(filter.protocol); } - if ('schema' in filter && filter.schema !== undefined) { - validateSchemaUrlNormalized(filter.schema); - } } return new EventsQuery(message); @@ -39,7 +36,7 @@ export class EventsQuery extends AbstractMessage{ const descriptor: EventsQueryDescriptor = { interface : DwnInterfaceName.Events, method : DwnMethodName.Query, - filters : options.filters ? Events.normalizeFilters(options.filters) : undefined, + filters : options.filters ? Events.normalizeFilters(options.filters) : [], messageTimestamp : options.messageTimestamp ?? Time.getCurrentTimestamp(), cursor : options.cursor, }; diff --git a/src/interfaces/events-subscribe.ts b/src/interfaces/events-subscribe.ts index fe82ace71..4941e96ef 100644 --- a/src/interfaces/events-subscribe.ts +++ b/src/interfaces/events-subscribe.ts @@ -5,8 +5,8 @@ import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { Time } from '../utils/time.js'; +import { validateProtocolUrlNormalized } from '../utils/url.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; -import { validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js'; export type EventsSubscribeOptions = { @@ -24,9 +24,6 @@ export class EventsSubscribe extends AbstractMessage { if ('protocol' in filter && filter.protocol !== undefined) { validateProtocolUrlNormalized(filter.protocol); } - if ('schema' in filter && filter.schema !== undefined) { - validateSchemaUrlNormalized(filter.schema); - } } Time.validateTimestamp(message.descriptor.messageTimestamp); diff --git a/src/types/events-types.ts b/src/types/events-types.ts index 733132612..6dfc1d0ee 100644 --- a/src/types/events-types.ts +++ b/src/types/events-types.ts @@ -1,39 +1,17 @@ import type { MessageEvent } from './subscriptions.js'; import type { AuthorizationModel, GenericMessage, GenericMessageReply, MessageSubscription } from './message-types.js'; import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; -import type { PaginationCursor, RangeCriterion, RangeFilter } from './query-types.js'; +import type { PaginationCursor, RangeCriterion } from './query-types.js'; /** * filters used when filtering for any type of Message across interfaces */ -export type EventsMessageFilter = { +export type EventsFilter = { interface?: string; method?: string; - dateUpdated?: RangeCriterion; -}; - -/** - * We only allow filtering for events by immutable properties, the omitted properties could be different per subsequent writes. - */ -export type EventsRecordsFilter = { - recipient?: string; protocol?: string; - protocolPath?: string; - contextId?: string; - schema?: string; - recordId?: string; - parentId?: string; - dataFormat?: string; - dataSize?: RangeFilter; - dateCreated?: RangeCriterion; + messageTimestamp?: RangeCriterion; }; - -/** - * A union type of the different types of filters a user can use when issuing an EventsQuery or EventsSubscribe - * TODO: simplify the EventsFilters to only the necessary in order to reduce complexity https://github.com/TBD54566975/dwn-sdk-js/issues/663 - */ -export type EventsFilter = EventsMessageFilter | EventsRecordsFilter; - export type MessageSubscriptionHandler = (event: MessageEvent) => void; export type EventsSubscribeMessageOptions = { @@ -60,7 +38,7 @@ export type EventsQueryDescriptor = { interface: DwnInterfaceName.Events; method: DwnMethodName.Query; messageTimestamp: string; - filters?: EventsFilter[]; + filters: EventsFilter[]; cursor?: PaginationCursor; }; diff --git a/src/utils/events.ts b/src/utils/events.ts index 64c965c2b..67cbcefa4 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -1,7 +1,9 @@ +import type { EventsFilter } from '../types/events-types.js'; import type { Filter } from '../types/query-types.js'; -import type { EventsFilter, EventsMessageFilter, EventsRecordsFilter } from '../types/events-types.js'; import { FilterUtility } from '../utils/filter.js'; +import { normalizeProtocolUrl } from './url.js'; +import { PermissionsProtocol } from '../protocols/permissions.js'; import { Records } from '../utils/records.js'; import { isEmptyObject, removeUndefinedProperties } from './object.js'; @@ -17,15 +19,15 @@ export class Events { const eventsQueryFilters: EventsFilter[] = []; - // normalize each filter individually by the type of filter it is. + // normalize each filter, and only add non-empty filters to the returned array for (const filter of filters) { - let eventsFilter: EventsFilter; - if (this.isRecordsFilter(filter)) { - eventsFilter = Records.normalizeFilter(filter); - } else { - // no normalization needed - eventsFilter = filter; - } + // normalize the protocol URL if it exists + const protocol = filter.protocol !== undefined ? normalizeProtocolUrl(filter.protocol) : undefined; + + const eventsFilter = { + ...filter, + protocol, + }; // remove any empty filter properties and do not add if empty removeUndefinedProperties(eventsFilter); @@ -34,7 +36,6 @@ export class Events { } } - return eventsQueryFilters; } @@ -53,43 +54,56 @@ export class Events { // first we check for `EventsRecordsFilter` fields for conversion // otherwise it is `EventsMessageFilter` fields for conversion for (const filter of filters) { - if (this.isRecordsFilter(filter)) { - eventsQueryFilters.push(Records.convertFilter(filter)); - } else { - eventsQueryFilters.push(this.convertFilter(filter)); + // extract the protocol tag filter from the incoming event record filter + // this filters for permission grants, requests and revocations associated with a targeted protocol + // since permissions are their own protocol, we added an additional tag index when writing the permission messages + // so that we can filter for permission records here + const permissionRecordsFilter = this.constructPermissionRecordsFilter(filter); + if (permissionRecordsFilter) { + eventsQueryFilters.push(permissionRecordsFilter); } + + eventsQueryFilters.push(this.convertFilter(filter)); } return eventsQueryFilters; } + /** + * Constructs a filter that gets associated permission records if protocol is in the given filter. + */ + private static constructPermissionRecordsFilter(filter: EventsFilter): Filter | undefined { + const { protocol, messageTimestamp } = filter; + if (protocol !== undefined) { + const taggedFilter = { + protocol: PermissionsProtocol.uri, + ...Records.convertTagsFilter({ protocol }) + } as Filter; + + if (messageTimestamp != undefined) { + // if we filter by message timestamp, we also want to filter the permission messages by the same timestamp range + const messageTimestampFilter = FilterUtility.convertRangeCriterion(messageTimestamp); + if (messageTimestampFilter) { + taggedFilter.messageTimestamp = messageTimestampFilter; + } + } + + return taggedFilter; + } + } + /** * Converts an external-facing filter model into an internal-facing filer model used by data store. */ - private static convertFilter(filter: EventsMessageFilter): Filter { + private static convertFilter(filter: EventsFilter): Filter { const filterCopy = { ...filter } as Filter; - const { dateUpdated } = filter; - const messageTimestampFilter = dateUpdated ? FilterUtility.convertRangeCriterion(dateUpdated) : undefined; + const { messageTimestamp } = filter; + const messageTimestampFilter = messageTimestamp ? FilterUtility.convertRangeCriterion(messageTimestamp) : undefined; if (messageTimestampFilter) { filterCopy.messageTimestamp = messageTimestampFilter; delete filterCopy.dateUpdated; } return filterCopy as Filter; } - - // we deliberately do not check for `dateUpdated` in this filter. - // if it were the only property that matched, it could be handled by `EventsFilter` - private static isRecordsFilter(filter: EventsFilter): filter is EventsRecordsFilter { - return 'author' in filter || - 'dateCreated' in filter || - 'dataFormat' in filter || - 'dataSize' in filter || - 'parentId' in filter || - 'recordId' in filter || - 'schema' in filter || - 'protocol' in filter || - 'protocolPath' in filter || - 'recipient' in filter; - } } \ No newline at end of file diff --git a/src/utils/records.ts b/src/utils/records.ts index 3e543881b..891786b54 100644 --- a/src/utils/records.ts +++ b/src/utils/records.ts @@ -322,7 +322,7 @@ export class Records { /** * This will create individual keys for each of the tag filters that look like `tag.tag_filter_property` */ - private static convertTagsFilter( tags: { [property: string]: RecordsWriteTagsFilter}): Filter { + public static convertTagsFilter( tags: { [property: string]: RecordsWriteTagsFilter}): Filter { const tagValues:Filter = {}; for (const property in tags) { const value = tags[property]; diff --git a/tests/handlers/events-query.spec.ts b/tests/handlers/events-query.spec.ts index c721d4394..d4c41a6a5 100644 --- a/tests/handlers/events-query.spec.ts +++ b/tests/handlers/events-query.spec.ts @@ -62,8 +62,7 @@ export function testEventsQueryHandler(): void { const bob = await TestDataGenerator.generateDidKeyPersona(); const { message } = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema1' }] + author: alice, }); const eventsQueryHandler = new EventsQueryHandler(didResolver, eventLog); const reply = await eventsQueryHandler.handle({ tenant: bob.did, message }); @@ -76,8 +75,7 @@ export function testEventsQueryHandler(): void { const alice = await TestDataGenerator.generateDidKeyPersona(); const { message } = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema1' }] + author: alice, }); (message['descriptor'] as any)['troll'] = 'hehe'; @@ -88,27 +86,12 @@ export function testEventsQueryHandler(): void { expect(reply.entries).to.not.exist; }); - it('returns 400 if no filters are provided', async () => { - const alice = await TestDataGenerator.generateDidKeyPersona(); - - const { message } = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema1' }], - }); // create with filter to prevent failure on .create() - message.descriptor.filters = []; // remove filters - const eventsQueryHandler = new EventsQueryHandler(didResolver, eventLog); - const reply = await eventsQueryHandler.handle({ tenant: alice.did, message }); - - expect(reply.status.code).to.equal(400); - expect(reply.entries).to.not.exist; - }); - it('returns 400 if an empty filter without properties is provided', async () => { const alice = await TestDataGenerator.generateDidKeyPersona(); const { message } = await TestDataGenerator.generateEventsQuery({ author : alice, - filters : [{ schema: 'schema1' }], + filters : [{ protocol: 'http://example.org/protocol/v1' }], }); // create with filter to prevent failure on .create() message.descriptor.filters = [{}]; // empty out filter properties const eventsQueryHandler = new EventsQueryHandler(didResolver, eventLog); diff --git a/tests/interfaces/events-query.spec.ts b/tests/interfaces/events-query.spec.ts index a7331c37a..0a3a841ca 100644 --- a/tests/interfaces/events-query.spec.ts +++ b/tests/interfaces/events-query.spec.ts @@ -1,6 +1,5 @@ import type { EventsQueryMessage } from '../../src/types/events-types.js'; import type { ProtocolsQueryFilter } from '../../src/types/protocols-types.js'; -import type { RecordsFilter } from '../../src/types/records-types.js'; import { EventsQuery } from '../../src/interfaces/events-query.js'; import { Jws } from '../../src/utils/jws.js'; @@ -20,7 +19,7 @@ describe('EventsQuery Message', () => { const currentTime = Time.getCurrentTimestamp(); const eventsQuery = await EventsQuery.create({ - filters : [{ schema: 'anything' }], + filters : [{ protocol: 'http://example.org/protocol/v1' }], messageTimestamp : currentTime, signer : Jws.createSigner(alice), }); @@ -43,30 +42,15 @@ describe('EventsQuery Message', () => { expect((message.descriptor.filters![0] as ProtocolsQueryFilter).protocol).to.eq('http://example.com'); }); - it('should auto-normalize schema URL', async () => { - const alice = await TestDataGenerator.generatePersona(); - - const options = { - recipient : alice.did, - signer : Jws.createSigner(alice), - filters : [{ schema: 'example.com/' }], - }; - const eventsQuery = await EventsQuery.create(options); - - const message = eventsQuery.message as EventsQueryMessage; - - expect(message.descriptor.filters?.length).to.equal(1); - expect((message.descriptor.filters![0] as RecordsFilter).schema).to.eq('http://example.com'); - }); - it('allows query with no filters', async () => { const alice = await TestDataGenerator.generatePersona(); const currentTime = Time.getCurrentTimestamp(); - const eventsQueryPromise = await EventsQuery.create({ + const eventsQuery = await EventsQuery.create({ messageTimestamp : currentTime, signer : Jws.createSigner(alice), }); - expect(eventsQueryPromise.message.descriptor.filters).to.be.undefined; + + expect(eventsQuery.message.descriptor.filters).to.deep.equal([]); // empty array }); it('removes empty filters', async () => { @@ -74,20 +58,21 @@ describe('EventsQuery Message', () => { const currentTime = Time.getCurrentTimestamp(); // single empty filter fails - const eventsQueryPromise = EventsQuery.create({ - filters : [{}], + const eventsQuery1 = await EventsQuery.create({ messageTimestamp : currentTime, signer : Jws.createSigner(alice), + filters : [{}], }); - await expect(eventsQueryPromise).to.eventually.be.rejectedWith('fewer than 1 items'); + expect(eventsQuery1.message.descriptor.filters).to.deep.equal([]); // empty array // empty filter gets removed, valid filter remains - const eventsQuery = await EventsQuery.create({ - filters : [{ schema: 'schema' },{ }], // one empty filter + const eventsQuery2 = await EventsQuery.create({ + filters : [{ protocol: 'http://example.org/protocol/v1' },{ }], // one empty filter messageTimestamp : currentTime, signer : Jws.createSigner(alice), }); - expect(eventsQuery.message.descriptor.filters?.length).to.equal(1); + expect(eventsQuery2.message.descriptor.filters?.length).to.equal(1); + expect(eventsQuery2.message.descriptor.filters).to.deep.equal([{ protocol: 'http://example.org/protocol/v1' }]); }); }); @@ -98,7 +83,7 @@ describe('EventsQuery Message', () => { const currentTime = Time.getCurrentTimestamp(); const eventsQuery = await EventsQuery.create({ - filters : [{ schema: 'anything' }], + filters : [{ protocol: 'http://example.org/protocol/v1' }], messageTimestamp : currentTime, signer : Jws.createSigner(alice), }); @@ -116,7 +101,7 @@ describe('EventsQuery Message', () => { const alice = await TestDataGenerator.generatePersona(); const currentTime = Time.getCurrentTimestamp(); const eventsQuery = await EventsQuery.create({ - filters : [{ schema: 'anything' }], + filters : [{ protocol: 'http://example.org/protocol/v1' }], messageTimestamp : currentTime, signer : Jws.createSigner(alice), }); @@ -137,14 +122,14 @@ describe('EventsQuery Message', () => { const { message } = eventsQuery; const parsedQuery = await EventsQuery.parse(message); - expect(parsedQuery.message.descriptor.filters).to.be.undefined; + expect(parsedQuery.message.descriptor.filters).to.deep.equal([]); }); it('throws an exception if message has an empty filter', async () => { const alice = await TestDataGenerator.generatePersona(); const currentTime = Time.getCurrentTimestamp(); const eventsQuery = await EventsQuery.create({ - filters : [{ schema: 'anything' }], + filters : [{ protocol: 'http://example.org/protocol/v1' }], messageTimestamp : currentTime, signer : Jws.createSigner(alice), }); diff --git a/tests/scenarios/events-query.spec.ts b/tests/scenarios/events-query.spec.ts index 9c47e1fc7..e42f251ea 100644 --- a/tests/scenarios/events-query.spec.ts +++ b/tests/scenarios/events-query.spec.ts @@ -8,14 +8,14 @@ import type { } from '../../src/index.js'; import freeForAll from '../vectors/protocol-definitions/free-for-all.json' assert { type: 'json' }; -import threadProtocol from '../vectors/protocol-definitions/thread-role.json' assert { type: 'json' }; import { expect } from 'chai'; +import { PermissionGrant } from '../../src/protocols/permission-grant.js'; import { TestDataGenerator } from '../utils/test-data-generator.js'; import { TestEventStream } from '../test-event-stream.js'; import { TestStores } from '../test-stores.js'; +import { DataStream, Dwn, DwnInterfaceName, DwnMethodName, Jws, Message, PermissionsProtocol, Time } from '../../src/index.js'; import { DidKey, UniversalResolver } from '@web5/dids'; -import { Dwn, DwnConstant, DwnInterfaceName, DwnMethodName, Message, Time } from '../../src/index.js'; export function testEventsQueryScenarios(): void { describe('events query tests', () => { @@ -68,8 +68,8 @@ export function testEventsQueryScenarios(): void { const eventsQueryRecords = await TestDataGenerator.generateEventsQuery({ author : alice, filters : [ - { recordId: record.message.recordId }, // RecordsWrite - { protocol: protocol.message.descriptor.definition.protocol } // ProtocolConfigure + { interface: DwnInterfaceName.Records }, // returns the RecordsWrite + { protocol: protocol.message.descriptor.definition.protocol } // returns the ProtocolConfigure ], }); const recordEventsReply = await dwn.processMessage(alice.did, eventsQueryRecords.message); @@ -200,19 +200,19 @@ export function testEventsQueryScenarios(): void { expect(recordsWriteEventsReplyAfterCursor.entries![0]).to.equal(await Message.getCid(record2Update.message)); }); - it('filters by a dateUpdated (messageTimestamp) range across different message types', async () => { + it('filters by a messageTimestamp range across different message types', async () => { // scenario: // alice creates (2) messages, (RecordsWrite and ProtocolsConfigure) - // each message on the first date of the year (2021, 2022 and 2023 respectively. + // each message on the first date of the year (2021, 2022 respectively. // alice queries for all records beyond the last day of 2021 and should return 1 of the 2 messages (ProtocolConfigure) // alice then creates a RecordsDelete message for the original RecordsWrite // alice queries once again however supplying a cursor of the last message from the prior query, returning the RecordsDelete message. const firstDayOf2021 = Time.createTimestamp({ year: 2021, month: 1, day: 1 }); - const firstDayOf2023 = Time.createTimestamp({ year: 2023, month: 1, day: 1 }); + const firstDayOf2022 = Time.createTimestamp({ year: 2022, month: 1, day: 1 }); const alice = await TestDataGenerator.generateDidKeyPersona(); const write = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: firstDayOf2021, messageTimestamp: firstDayOf2021 }); - const protocol = await TestDataGenerator.generateProtocolsConfigure({ author: alice, messageTimestamp: firstDayOf2023 }); + const protocol = await TestDataGenerator.generateProtocolsConfigure({ author: alice, messageTimestamp: firstDayOf2022 }); // insert data const writeReply = await dwn.processMessage(alice.did, write.message, { dataStream: write.dataStream }); @@ -224,7 +224,7 @@ export function testEventsQueryScenarios(): void { const lastDayOf2021 = Time.createTimestamp({ year: 2021, month: 12, day: 31 }); let eventsQuery1 = await TestDataGenerator.generateEventsQuery({ author : alice, - filters : [{ dateUpdated: { from: lastDayOf2021 } }], + filters : [{ messageTimestamp: { from: lastDayOf2021 } }], }); let reply1 = await dwn.processMessage(alice.did, eventsQuery1.message); expect(reply1.status.code).to.equal(200); @@ -238,7 +238,7 @@ export function testEventsQueryScenarios(): void { eventsQuery1 = await TestDataGenerator.generateEventsQuery({ author : alice, - filters : [{ dateUpdated: { from: lastDayOf2021 } }], + filters : [{ messageTimestamp: { from: lastDayOf2021 } }], cursor : reply1.cursor }); reply1 = await dwn.processMessage(alice.did, eventsQuery1.message); @@ -247,133 +247,26 @@ export function testEventsQueryScenarios(): void { expect(reply1.entries![0]).to.equal(await Message.getCid(delete1.message!)); }); - it('filters by dateCreated', async () => { - // scenario: 4 records, created on first of 2021, 2022, 2023, 2024 respectively, only the first 2 records - const firstDayOf2021 = Time.createTimestamp({ year: 2021, month: 1, day: 1 }); - const firstDayOf2022 = Time.createTimestamp({ year: 2022, month: 1, day: 1 }); - const firstDayOf2023 = Time.createTimestamp({ year: 2023, month: 1, day: 1 }); - const firstDayOf2024 = Time.createTimestamp({ year: 2024, month: 1, day: 1 }); - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const write1 = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: firstDayOf2021, messageTimestamp: firstDayOf2021 }); - const write2 = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: firstDayOf2022, messageTimestamp: firstDayOf2022 }); - const write3 = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: firstDayOf2023, messageTimestamp: firstDayOf2023 }); - const write4 = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: firstDayOf2024, messageTimestamp: firstDayOf2024 }); - - // insert data - const writeReply1 = await dwn.processMessage(alice.did, write1.message, { dataStream: write1.dataStream }); - const writeReply2 = await dwn.processMessage(alice.did, write2.message, { dataStream: write2.dataStream }); - const writeReply3 = await dwn.processMessage(alice.did, write3.message, { dataStream: write3.dataStream }); - const writeReply4 = await dwn.processMessage(alice.did, write4.message, { dataStream: write4.dataStream }); - expect(writeReply1.status.code).to.equal(202); - expect(writeReply2.status.code).to.equal(202); - expect(writeReply3.status.code).to.equal(202); - expect(writeReply4.status.code).to.equal(202); - - // testing `from` range with a limit - const lastDayOf2021 = Time.createTimestamp({ year: 2021, month: 12, day: 31 }); - let fromLastDayOf2021 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { from: lastDayOf2021 } }], - }); - let fromLastDayOf2021Reply = await dwn.processMessage(alice.did, fromLastDayOf2021.message); - expect(fromLastDayOf2021Reply.status.code).to.equal(200); - expect(fromLastDayOf2021Reply.entries?.length).to.equal(3); - expect(fromLastDayOf2021Reply.entries![0]).to.equal(await Message.getCid(write2.message!)); - expect(fromLastDayOf2021Reply.entries![1]).to.equal(await Message.getCid(write3.message!)); - expect(fromLastDayOf2021Reply.entries![2]).to.equal(await Message.getCid(write4.message!)); - - // testing `to` range - const lastDayOf2022 = Time.createTimestamp({ year: 2022, month: 12, day: 31 }); - let toLastDayOf2022 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { to: lastDayOf2022 } }], - }); - let toLastDayOf2022Reply = await dwn.processMessage(alice.did, toLastDayOf2022.message); - expect(toLastDayOf2022Reply.status.code).to.equal(200); - expect(toLastDayOf2022Reply.entries?.length).to.equal(2); - expect(toLastDayOf2022Reply.entries![0]).to.equal(await Message.getCid(write1.message!)); - expect(toLastDayOf2022Reply.entries![1]).to.equal(await Message.getCid(write2.message!)); - - // testing `from` and `to` range - const lastDayOf2023 = Time.createTimestamp({ year: 2023, month: 12, day: 31 }); - let fromLastDay2022ToLastDay2023 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { from: lastDayOf2022, to: lastDayOf2023 } }], - }); - let fromLastDayOf2022ToLastDay2023Reply = await dwn.processMessage(alice.did, fromLastDay2022ToLastDay2023.message); - expect(fromLastDayOf2022ToLastDay2023Reply.status.code).to.equal(200); - expect(fromLastDayOf2022ToLastDay2023Reply.entries?.length).to.equal(1); - expect(fromLastDayOf2022ToLastDay2023Reply.entries![0]).to.equal(await Message.getCid(write3.message!)); - - // testing edge case where value equals `from` and `to` - let fromFirstDay2022ToFirstDay2023 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { from: firstDayOf2022, to: firstDayOf2023 } }], - }); - let fromFirstDay2022ToFirstDay2023Reply = await dwn.processMessage(alice.did, fromFirstDay2022ToFirstDay2023.message); - expect(fromFirstDay2022ToFirstDay2023Reply.status.code).to.equal(200); - expect(fromFirstDay2022ToFirstDay2023Reply.entries?.length).to.equal(1); - expect(fromFirstDay2022ToFirstDay2023Reply.entries![0]).to.equal(await Message.getCid(write2.message!)); - - // add an additional records to match against the previous queries - const write5 = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: lastDayOf2022, messageTimestamp: lastDayOf2022 }); - const writeReply5 = await dwn.processMessage(alice.did, write5.message, { dataStream: write5.dataStream }); - expect(writeReply5.status.code).to.equal(202); - const write6 = await TestDataGenerator.generateRecordsWrite({ author: alice, dateCreated: firstDayOf2021, messageTimestamp: firstDayOf2021 }); - const writeReply6 = await dwn.processMessage(alice.did, write6.message, { dataStream: write6.dataStream }); - expect(writeReply6.status.code).to.equal(202); - - fromLastDayOf2021 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { from: lastDayOf2021 } }], - cursor : fromLastDayOf2021Reply.cursor - }); - fromLastDayOf2021Reply = await dwn.processMessage(alice.did, fromLastDayOf2021.message); - expect(fromLastDayOf2021Reply.status.code).to.equal(200); - expect(fromLastDayOf2021Reply.entries?.length).to.equal(1); - expect(fromLastDayOf2021Reply.entries![0]).to.equal(await Message.getCid(write5.message!)); - - toLastDayOf2022 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { to: lastDayOf2022 } }], - cursor : toLastDayOf2022Reply.cursor, - }); - toLastDayOf2022Reply = await dwn.processMessage(alice.did, toLastDayOf2022.message); - expect(toLastDayOf2022Reply.status.code).to.equal(200); - expect(toLastDayOf2022Reply.entries?.length).to.equal(1); - expect(toLastDayOf2022Reply.entries![0]).to.equal(await Message.getCid(write6.message!)); - - fromLastDay2022ToLastDay2023 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { from: lastDayOf2022, to: lastDayOf2023 } }], - cursor : fromLastDayOf2022ToLastDay2023Reply.cursor, - }); - fromLastDayOf2022ToLastDay2023Reply = await dwn.processMessage(alice.did, fromLastDay2022ToLastDay2023.message); - expect(fromLastDayOf2022ToLastDay2023Reply.status.code).to.equal(200); - expect(fromLastDayOf2022ToLastDay2023Reply.entries?.length).to.equal(1); - expect(fromLastDayOf2021Reply.entries![0]).to.equal(await Message.getCid(write5.message!)); + it('filters by a protocol across different message types', async () => { + // NOTE: This test validates the ability to filter by a specific protocol across different message types. + // This will return any of the `RecordsWrite`, `RecordsDelete` and `ProtocolConfigure` messages that are associated with the protocol + // Additionally this will return permission-protocol `RecordsWrite` messages that are associated with the protocol. - // testing edge case where value equals `from` and `to` - fromFirstDay2022ToFirstDay2023 = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ dateCreated: { from: firstDayOf2022, to: firstDayOf2023 } }], - cursor : fromFirstDay2022ToFirstDay2023Reply.cursor, - }); - fromFirstDay2022ToFirstDay2023Reply = await dwn.processMessage(alice.did, fromFirstDay2022ToFirstDay2023.message); - expect(fromFirstDay2022ToFirstDay2023Reply.status.code).to.equal(200); - expect(fromFirstDay2022ToFirstDay2023Reply.entries?.length).to.equal(1); - expect(fromLastDayOf2021Reply.entries![0]).to.equal(await Message.getCid(write5.message!)); - }); + // `RecordsDelete` messages associated with requests/grants/revocations are not yet indexed. + // TODO: https://github.com/TBD54566975/dwn-sdk-js/issues/768 - it('filters by a protocol across different message types', async () => { // scenario: - // alice creates (3) different message types all related to "proto1" (Configure, RecordsWrite, RecordsDelete) - // alice creates (3) different message types all related to "proto2" (Configure, RecordsWrite, RecordsDelete) + // alice configures two different protocols (proto1, proto2) + // alice creates records for each protocol + // bob requests permissions for both protocols + // alice grants bob permissions for both protocols // when issuing an EventsQuery for the specific protocol, only Events related to it should be returned. - // alice then creates an additional messages to query after a cursor + // alice then deletes the records for each protocol + // alice revokes bob's permissions for both protocols + // now when issuing an EventsQuery for the specific protocol givin a cursor, only the latest event should be returned. const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); // create a proto1 const protoConf1 = await TestDataGenerator.generateProtocolsConfigure({ @@ -381,12 +274,6 @@ export function testEventsQueryScenarios(): void { protocolDefinition : { ...freeForAll, protocol: 'proto1' } }); - const postProperties = { - protocolPath : 'post', - schema : freeForAll.types.post.schema, - dataFormat : freeForAll.types.post.dataFormats[0], - }; - const proto1 = protoConf1.message.descriptor.definition.protocol; const protoConf1Response = await dwn.processMessage(alice.did, protoConf1.message); expect(protoConf1Response.status.code).equals(202); @@ -400,6 +287,12 @@ export function testEventsQueryScenarios(): void { const protoConf2Response = await dwn.processMessage(alice.did, protoConf2.message); expect(protoConf2Response.status.code).equals(202); + const postProperties = { + protocolPath : 'post', + schema : freeForAll.types.post.schema, + dataFormat : freeForAll.types.post.dataFormats[0], + }; + // create a record for proto1 const write1proto1 = await TestDataGenerator.generateRecordsWrite({ author: alice, protocol: proto1, ...postProperties }); const write1Response = await dwn.processMessage(alice.did, write1proto1.message, { dataStream: write1proto1.dataStream }); @@ -410,18 +303,74 @@ export function testEventsQueryScenarios(): void { const write1Proto2Response = await dwn.processMessage(alice.did, write1proto2.message, { dataStream: write1proto2.dataStream }); expect(write1Proto2Response.status.code).equals(202); - // filter for proto1 + // bob requests permissions for proto 1 + const requestProto1 = await PermissionsProtocol.createRequest({ + signer : Jws.createSigner(bob), + scope : { interface: DwnInterfaceName.Records, method: DwnMethodName.Write, protocol: proto1 }, + delegated : false, + }); + const requestProto1Response = await dwn.processMessage( + alice.did, + requestProto1.recordsWrite.message, + { dataStream: DataStream.fromBytes(requestProto1.permissionRequestBytes) } + ); + expect(requestProto1Response.status.code).equals(202); + + // bob requests permissions for proto 2 + const requestProto2 = await PermissionsProtocol.createRequest({ + signer : Jws.createSigner(bob), + scope : { interface: DwnInterfaceName.Records, method: DwnMethodName.Write, protocol: proto2 }, + delegated : false, + }); + const requestProto2Response = await dwn.processMessage( + alice.did, + requestProto2.recordsWrite.message, + { dataStream: DataStream.fromBytes(requestProto2.permissionRequestBytes) } + ); + expect(requestProto2Response.status.code).equals(202); + + // alice grants bob permissions for proto 1 + const grantProto1 = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + scope : requestProto1.permissionRequestData.scope, + dateExpires : Time.createOffsetTimestamp({ seconds: 5 }), + grantedTo : bob.did, + }); + const grantProto1Response = await dwn.processMessage( + alice.did, + grantProto1.recordsWrite.message, + { dataStream: DataStream.fromBytes(grantProto1.permissionGrantBytes) } + ); + expect(grantProto1Response.status.code).equals(202); + + // alice grants bob permissions for proto 2 + const grantProto2 = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + scope : requestProto2.permissionRequestData.scope, + dateExpires : Time.createOffsetTimestamp({ seconds: 5 }), + grantedTo : bob.did, + }); + const grantProto2Response = await dwn.processMessage( + alice.did, + grantProto2.recordsWrite.message, + { dataStream: DataStream.fromBytes(grantProto2.permissionGrantBytes) } + ); + expect(grantProto2Response.status.code).equals(202); + + // filter for proto1 events let proto1EventsQuery = await TestDataGenerator.generateEventsQuery({ author : alice, filters : [{ protocol: proto1 }] }); let proto1EventsReply = await dwn.processMessage(alice.did, proto1EventsQuery.message); expect(proto1EventsReply.status.code).equals(200); - expect(proto1EventsReply.entries?.length).equals(2); - - // check order of events returned. - expect(proto1EventsReply.entries![0]).to.equal(await Message.getCid(protoConf1.message)); - expect(proto1EventsReply.entries![1]).to.equal(await Message.getCid(write1proto1.message)); + expect(proto1EventsReply.entries?.length).equals(4); // configure, write, request, grant + expect(proto1EventsReply.entries).to.have.members([ + await Message.getCid(protoConf1.message), + await Message.getCid(write1proto1.message), + await Message.getCid(requestProto1.recordsWrite.message), + await Message.getCid(grantProto1.recordsWrite.message), + ]); // filter for proto2 let proto2EventsQuery = await TestDataGenerator.generateEventsQuery({ @@ -430,11 +379,13 @@ export function testEventsQueryScenarios(): void { }); let proto2EventsReply = await dwn.processMessage(alice.did, proto2EventsQuery.message); expect(proto2EventsReply.status.code).equals(200); - expect(proto2EventsReply.entries?.length).equals(2); - - // check order of events returned. - expect(proto2EventsReply.entries![0]).to.equal(await Message.getCid(protoConf2.message)); - expect(proto2EventsReply.entries![1]).to.equal(await Message.getCid(write1proto2.message)); + expect(proto2EventsReply.entries?.length).equals(4); // configure, write, request, grant + expect(proto2EventsReply.entries).to.have.members([ + await Message.getCid(protoConf2.message), + await Message.getCid(write1proto2.message), + await Message.getCid(requestProto2.recordsWrite.message), + await Message.getCid(grantProto2.recordsWrite.message), + ]); // delete proto1 message const deleteProto1Message = await TestDataGenerator.generateRecordsDelete({ author: alice, recordId: write1proto1.message.recordId }); @@ -446,6 +397,30 @@ export function testEventsQueryScenarios(): void { const deleteProto2MessageReply = await dwn.processMessage(alice.did, deleteProto2Message.message); expect(deleteProto2MessageReply.status.code).to.equal(202); + // revoke permissions for proto1 + const revokeProto1 = await PermissionsProtocol.createRevocation({ + signer : Jws.createSigner(alice), + grant : await PermissionGrant.parse(grantProto1.dataEncodedMessage), + }); + const revokeProto1Response = await dwn.processMessage( + alice.did, + revokeProto1.recordsWrite.message, + { dataStream: DataStream.fromBytes(revokeProto1.permissionRevocationBytes) } + ); + expect(revokeProto1Response.status.code).equals(202); + + // revoke permissions for proto2 + const revokeProto2 = await PermissionsProtocol.createRevocation({ + signer : Jws.createSigner(alice), + grant : await PermissionGrant.parse(grantProto2.dataEncodedMessage), + }); + const revokeProto2Response = await dwn.processMessage( + alice.did, + revokeProto2.recordsWrite.message, + { dataStream: DataStream.fromBytes(revokeProto2.permissionRevocationBytes) } + ); + expect(revokeProto2Response.status.code).equals(202); + //query messages beyond the cursor proto1EventsQuery = await TestDataGenerator.generateEventsQuery({ author : alice, @@ -454,8 +429,11 @@ export function testEventsQueryScenarios(): void { }); proto1EventsReply = await dwn.processMessage(alice.did, proto1EventsQuery.message); expect(proto1EventsReply.status.code).equals(200); - expect(proto1EventsReply.entries?.length).equals(1); - expect(proto1EventsReply.entries![0]).to.equal(await Message.getCid(deleteProto1Message.message)); + expect(proto1EventsReply.entries?.length).equals(2); // delete, revoke + expect(proto1EventsReply.entries).to.have.members([ + await Message.getCid(deleteProto1Message.message), + await Message.getCid(revokeProto1.recordsWrite.message), + ]); //query messages beyond the cursor proto2EventsQuery = await TestDataGenerator.generateEventsQuery({ @@ -465,649 +443,31 @@ export function testEventsQueryScenarios(): void { }); proto2EventsReply = await dwn.processMessage(alice.did, proto2EventsQuery.message); expect(proto2EventsReply.status.code).equals(200); - expect(proto2EventsReply.entries?.length).equals(1); - expect(proto2EventsReply.entries![0]).to.equal(await Message.getCid(deleteProto2Message.message)); - }); - - it('filters by protocol, protocolPath & parentId', async () => { - // scenario: get all messages across a protocol & protocolPath combo - // alice installs a protocol and creates a thread - // alice adds bob and carol as participants - // alice, bob, and carol all create messages - // alice filter for 'thread', 'thread/participants' and 'thread/messages' - // alice deletes carol participant message - // alice filters for 'thread/participant' after a cursor - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); - const carol = await TestDataGenerator.generateDidKeyPersona(); - - // create protocol - const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ - author : alice, - protocolDefinition : { ...threadProtocol } - }); - const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); - expect(protocolConfigureReply.status.code).to.equal(202); - const protocol = protocolConfigure.message.descriptor.definition.protocol; - - // alice creates thread - const thread = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocol, - protocolPath : 'thread' - }); - const threadReply = await dwn.processMessage(alice.did, thread.message, { dataStream: thread.dataStream }); - expect(threadReply.status.code).to.equal(202); - - // add bob as participant - const bobParticipant = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : bob.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/participant' - }); - const bobParticipantReply = await dwn.processMessage(alice.did, bobParticipant.message, { dataStream: bobParticipant.dataStream }); - expect(bobParticipantReply.status.code).to.equal(202); - - // add carol as participant - const carolParticipant = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : carol.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/participant' - }); - const carolParticipantReply = await dwn.processMessage(alice.did, carolParticipant.message, { dataStream: carolParticipant.dataStream }); - expect(carolParticipantReply.status.code).to.equal(202); - - // add a message to protocol1 - const message1 = await TestDataGenerator.generateRecordsWrite({ - author : bob, - recipient : alice.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - protocolRole : 'thread/participant', - }); - const message1Reply = await dwn.processMessage(alice.did, message1.message, { dataStream: message1.dataStream }); - expect(message1Reply.status.code).to.equal(202); - - const message2 = await TestDataGenerator.generateRecordsWrite({ - author : bob, - recipient : alice.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - protocolRole : 'thread/participant', - }); - const message2Reply = await dwn.processMessage(alice.did, message2.message, { dataStream: message2.dataStream }); - expect(message2Reply.status.code).to.equal(202); - - const message3 = await TestDataGenerator.generateRecordsWrite({ - author : carol, - recipient : alice.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - protocolRole : 'thread/participant', - }); - const message3Reply = await dwn.processMessage(alice.did, message3.message, { dataStream: message3.dataStream }); - expect(message3Reply.status.code).to.equal(202); - - // query for thread - const threadQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ protocol: protocol, protocolPath: 'thread' }], - }); - const threadQueryReply = await dwn.processMessage(alice.did, threadQuery.message); - expect(threadQueryReply.status.code).to.equal(200); - expect(threadQueryReply.entries?.length).to.equal(1); - expect(threadQueryReply.entries![0]).to.equal(await Message.getCid(thread.message)); - - // query for participants - let participantsQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ protocol: protocol, protocolPath: 'thread/participant', parentId: thread.message.recordId }], - }); - let participantsQueryReply = await dwn.processMessage(alice.did, participantsQuery.message); - expect(participantsQueryReply.status.code).to.equal(200); - expect(participantsQueryReply.entries?.length).to.equal(2); - expect(participantsQueryReply.entries![0]).to.equal(await Message.getCid(bobParticipant.message)); - expect(participantsQueryReply.entries![1]).to.equal(await Message.getCid(carolParticipant.message)); - - // query for chats - let chatQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ protocol: protocol, protocolPath: 'thread/chat', parentId: thread.message.recordId }], - }); - let chatQueryReply = await dwn.processMessage(alice.did, chatQuery.message); - expect(chatQueryReply.status.code).to.equal(200); - expect(chatQueryReply.entries?.length).to.equal(3); - expect(chatQueryReply.entries![0]).to.equal(await Message.getCid(message1.message)); - expect(chatQueryReply.entries![1]).to.equal(await Message.getCid(message2.message)); - expect(chatQueryReply.entries![2]).to.equal(await Message.getCid(message3.message)); - - // delete carol participant - const deleteCarol = await TestDataGenerator.generateRecordsDelete({ - author : alice, - recordId : carolParticipant.message.recordId - }); - const deleteCarolReply = await dwn.processMessage(alice.did, deleteCarol.message); - expect(deleteCarolReply.status.code).to.equal(202); - - // query for participants past the cursor - participantsQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ protocol: protocol, protocolPath: 'thread/participant', parentId: thread.message.recordId }], - cursor : participantsQueryReply.cursor - }); - participantsQueryReply = await dwn.processMessage(alice.did, participantsQuery.message); - expect(participantsQueryReply.status.code).to.equal(200); - expect(participantsQueryReply.entries?.length).to.equal(1); - expect(participantsQueryReply.entries![0]).to.equal(await Message.getCid(deleteCarol.message)); - - // query for chats beyond the cursor as a control, should have none. - chatQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ protocol: protocol, protocolPath: 'thread/chat', parentId: thread.message.recordId }], - cursor : chatQueryReply.cursor - }); - chatQueryReply = await dwn.processMessage(alice.did, chatQuery.message); - expect(chatQueryReply.status.code).to.equal(200); - expect(chatQueryReply.entries?.length).to.equal(0); - }); - - it('filters by recipient', async () => { - // scenario: alice installs a free-for-all protocol and makes posts with both bob and carol as recipients - // carol and bob also make posts with alice as a recipient - // alice queries for events meant for specific recipients - // alice then makes another message to query for using the pervious as a cursor - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); - const carol = await TestDataGenerator.generateDidKeyPersona(); - - 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 protocol = protocolConfigure.message.descriptor.definition.protocol; - - const postProperties = { - protocol : protocol, - protocolPath : 'post', - schema : freeForAll.types.post.schema, - dataFormat : freeForAll.types.post.dataFormats[0], - }; - - const messageFromAliceToBob = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : alice, - recipient : bob.did, - }); - const messageFromAliceToBobReply = - await dwn.processMessage(alice.did, messageFromAliceToBob.message, { dataStream: messageFromAliceToBob.dataStream }); - expect(messageFromAliceToBobReply.status.code).to.equal(202); - - const messageFromAliceToCarol = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : alice, - recipient : carol.did, - }); - const messageFromAliceToCarolReply = - await dwn.processMessage(alice.did, messageFromAliceToCarol.message, { dataStream: messageFromAliceToCarol.dataStream }); - expect(messageFromAliceToCarolReply.status.code).to.equal(202); - - const messageFromBobToAlice = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : bob, - recipient : alice.did, - }); - const messageFromBobToAliceReply = - await dwn.processMessage(alice.did, messageFromBobToAlice.message, { dataStream: messageFromBobToAlice.dataStream }); - expect(messageFromBobToAliceReply.status.code).to.equal(202); - - const messageFromCarolToAlice = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : carol, - recipient : alice.did, - }); - const messageFromCarolToAliceReply = - await dwn.processMessage(alice.did, messageFromCarolToAlice.message, { dataStream: messageFromCarolToAlice.dataStream }); - expect(messageFromCarolToAliceReply.status.code).to.equal(202); - - let authorQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ recipient: alice.did }] - }); - let authorQueryReply = await dwn.processMessage(alice.did, authorQuery.message); - expect(authorQueryReply.status.code).to.equal(200); - expect(authorQueryReply.entries?.length).to.equal(2); - expect(authorQueryReply.entries![0]).to.equal(await Message.getCid(messageFromBobToAlice.message)); - expect(authorQueryReply.entries![1]).to.equal(await Message.getCid(messageFromCarolToAlice.message)); - - authorQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ recipient: bob.did }] - }); - authorQueryReply = await dwn.processMessage(alice.did, authorQuery.message); - expect(authorQueryReply.status.code).to.equal(200); - expect(authorQueryReply.entries?.length).to.equal(1); - expect(authorQueryReply.entries![0]).to.equal(await Message.getCid(messageFromAliceToBob.message)); - - - // add another message - const messageFromAliceToBob2 = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : alice, - recipient : bob.did, - }); - const messageFromAliceToBob2Reply = - await dwn.processMessage(alice.did, messageFromAliceToBob2.message, { dataStream: messageFromAliceToBob2.dataStream }); - expect(messageFromAliceToBob2Reply.status.code).to.equal(202); - - authorQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ recipient: bob.did }], - cursor : authorQueryReply.cursor, - }); - - authorQueryReply = await dwn.processMessage(alice.did, authorQuery.message); - expect(authorQueryReply.status.code).to.equal(200); - expect(authorQueryReply.entries?.length).to.equal(1); - expect(authorQueryReply.entries![0]).to.equal(await Message.getCid(messageFromAliceToBob2.message)); - }); - - it('filters by schema', async () => { - const alice = await TestDataGenerator.generateDidKeyPersona(); - - const schema1Message1 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema1' - }); - const schema1Message1Reply = await dwn.processMessage(alice.did, schema1Message1.message, { dataStream: schema1Message1.dataStream }); - expect(schema1Message1Reply.status.code).to.equal(202); - - const schema2Message1 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema2' - }); - const schema2Message1Reply = await dwn.processMessage(alice.did, schema2Message1.message, { dataStream: schema2Message1.dataStream }); - expect(schema2Message1Reply.status.code).to.equal(202); - - const schema2Message2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema2' - }); - const schema2Message2Reply = await dwn.processMessage(alice.did, schema2Message2.message, { dataStream: schema2Message2.dataStream }); - expect(schema2Message2Reply.status.code).to.equal(202); - - let schema1Query = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema1' }], - }); - let schema1QueryReply = await dwn.processMessage(alice.did, schema1Query.message); - expect(schema1QueryReply.status.code).to.equal(200); - expect(schema1QueryReply.entries?.length).to.equal(1); - expect(schema1QueryReply.entries![0]).to.equal(await Message.getCid(schema1Message1.message)); - - let schema2Query = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema2' }], - }); - let schema2QueryReply = await dwn.processMessage(alice.did, schema2Query.message); - expect(schema2QueryReply.status.code).to.equal(200); - expect(schema2QueryReply.entries?.length).to.equal(2); - expect(schema2QueryReply.entries![0]).to.equal(await Message.getCid(schema2Message1.message)); - expect(schema2QueryReply.entries![1]).to.equal(await Message.getCid(schema2Message2.message)); - - const schema1Message2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema1' - }); - const schema1Message2Reply = await dwn.processMessage(alice.did, schema1Message2.message, { dataStream: schema1Message2.dataStream }); - expect(schema1Message2Reply.status.code).to.equal(202); - - schema1Query = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema1' }], - cursor : schema1QueryReply.cursor, - }); - schema1QueryReply = await dwn.processMessage(alice.did, schema1Query.message); - expect(schema1QueryReply.status.code).to.equal(200); - expect(schema1QueryReply.entries?.length).to.equal(1); - expect(schema1QueryReply.entries![0]).to.equal(await Message.getCid(schema1Message2.message)); - - schema2Query = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ schema: 'schema2' }], - cursor : schema2QueryReply.cursor, - }); - schema2QueryReply = await dwn.processMessage(alice.did, schema2Query.message); - expect(schema2QueryReply.status.code).to.equal(200); - expect(schema2QueryReply.entries?.length).to.equal(0); - }); - - it('filters by recordId', async () => { - const alice = await TestDataGenerator.generateDidKeyPersona(); - - // a write as a control, will not show up in query - const controlWrite = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema1' - }); - const write2Reply = await dwn.processMessage(alice.did, controlWrite.message, { dataStream: controlWrite.dataStream }); - expect(write2Reply.status.code).to.equal(202); - - const write = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema1' - }); - const write1Reply = await dwn.processMessage(alice.did, write.message, { dataStream: write.dataStream }); - expect(write1Reply.status.code).to.equal(202); - - const update = await TestDataGenerator.generateFromRecordsWrite({ - author : alice, - existingWrite : write.recordsWrite, - }); - const updateReply = await dwn.processMessage(alice.did, update.message, { dataStream: update.dataStream }); - expect(updateReply.status.code).to.equal(202); - - let recordQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ recordId: write.message.recordId }], - }); - let recordQueryReply = await dwn.processMessage(alice.did, recordQuery.message); - expect(recordQueryReply.status.code).to.equal(200); - expect(recordQueryReply.entries?.length).to.equal(2); - expect(recordQueryReply.entries![0]).to.equal(await Message.getCid(write.message)); - expect(recordQueryReply.entries![1]).to.equal(await Message.getCid(update.message)); - - const deleteRecord = await TestDataGenerator.generateRecordsDelete({ - author : alice, - recordId : write.message.recordId, - }); - const deleteRecordReply = await dwn.processMessage(alice.did, deleteRecord.message); - expect(deleteRecordReply.status.code).to.equal(202); - - recordQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ recordId: write.message.recordId }], - cursor : recordQueryReply.cursor, - }); - recordQueryReply = await dwn.processMessage(alice.did, recordQuery.message); - expect(recordQueryReply.status.code).to.equal(200); - expect(recordQueryReply.entries?.length).to.equal(1); - expect(recordQueryReply.entries![0]).to.equal(await Message.getCid(deleteRecord.message)); - }); - - it('filters by dataFormat', async () => { - // scenario: alice stores different file types and needs events relating to `image/jpeg` - // alice creates 3 files, one of them `image/jpeg` - // alice queries for `image/jpeg` retrieving the one message - // alice adds another image to query for using the prior image as a cursor - - const alice = await TestDataGenerator.generateDidKeyPersona(); - - const textFile = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'application/text' - }); - const textFileReply = await dwn.processMessage(alice.did, textFile.message, { dataStream: textFile.dataStream }); - expect(textFileReply.status.code).to.equal(202); - - const jsonData = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'application/json' - }); - const jsonDataReply = await dwn.processMessage(alice.did, jsonData.message, { dataStream: jsonData.dataStream }); - expect(jsonDataReply.status.code).to.equal(202); - - const imageData = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'image/jpeg' - }); - const imageDataReply = await dwn.processMessage(alice.did, imageData.message, { dataStream: imageData.dataStream }); - expect(imageDataReply.status.code).to.equal(202); - - //get image data - let imageQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ - dataFormat: 'image/jpeg' - }] - }); - let imageQueryReply = await dwn.processMessage(alice.did, imageQuery.message); - expect(imageQueryReply.status.code).to.equal(200); - expect(imageQueryReply.entries?.length).to.equal(1); - expect(imageQueryReply.entries![0]).to.equal(await Message.getCid(imageData.message)); - - // add another image - const imageData2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'image/jpeg' - }); - const imageData2Reply = await dwn.processMessage(alice.did, imageData2.message, { dataStream: imageData2.dataStream }); - expect(imageData2Reply.status.code).to.equal(202); - - imageQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ - dataFormat: 'image/jpeg' - }], - cursor: imageQueryReply.cursor, - }); - imageQueryReply = await dwn.processMessage(alice.did, imageQuery.message); - expect(imageQueryReply.status.code).to.equal(200); - expect(imageQueryReply.entries?.length).to.equal(1); - expect(imageQueryReply.entries![0]).to.equal(await Message.getCid(imageData2.message)); - });; - - it('filters by dataSize', async () => { - // scenario: - // alice inserts both small and large data - // alice requests events for messages with data size under a threshold - - const alice = await TestDataGenerator.generateDidKeyPersona(); - - const smallSize1 = await TestDataGenerator.generateRecordsWrite({ - author: alice, - }); - const smallSize1Reply = await dwn.processMessage(alice.did, smallSize1.message, { dataStream: smallSize1.dataStream }); - expect(smallSize1Reply.status.code).to.equal(202); - - const largeSize = await TestDataGenerator.generateRecordsWrite({ - author : alice, - data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded + 1) - }); - const largeSizeReply = await dwn.processMessage(alice.did, largeSize.message, { dataStream: largeSize.dataStream }); - expect(largeSizeReply.status.code).to.equal(202); - - const smallSize2 = await TestDataGenerator.generateRecordsWrite({ - author: alice, - }); - const smallSize2Reply = await dwn.processMessage(alice.did, smallSize2.message, { dataStream: smallSize2.dataStream }); - expect(smallSize2Reply.status.code).to.equal(202); - - //get large sizes - let largeSizeQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ - dataSize: { gte: DwnConstant.maxDataSizeAllowedToBeEncoded + 1 } - }] - }); - let largeSizeQueryReply = await dwn.processMessage(alice.did, largeSizeQuery.message); - expect(largeSizeQueryReply.status.code).to.equal(200); - expect(largeSizeQueryReply.entries?.length).to.equal(1); - expect(largeSizeQueryReply.entries![0]).to.equal(await Message.getCid(largeSize.message)); - - const largeSize2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded + 1) - }); - const largeSize2Reply = await dwn.processMessage(alice.did, largeSize2.message, { dataStream: largeSize2.dataStream }); - expect(largeSize2Reply.status.code).to.equal(202); - - largeSizeQuery = await TestDataGenerator.generateEventsQuery({ - author : alice, - filters : [{ - dataSize: { gte: DwnConstant.maxDataSizeAllowedToBeEncoded + 1 } - }], - cursor: largeSizeQueryReply.cursor, - }); - largeSizeQueryReply = await dwn.processMessage(alice.did, largeSizeQuery.message); - expect(largeSizeQueryReply.status.code).to.equal(200); - expect(largeSizeQueryReply.entries?.length).to.equal(1); - expect(largeSizeQueryReply.entries![0]).to.equal(await Message.getCid(largeSize2.message)); - }); - - it('filters by contextId', async () => { - // scenario: - // alice configures a chat protocols and creates 2 chat threads - // alice invites bob as participant in thread1 and carol in thread2 - // alice writes messages to both bob and carol in their respective threads - // alice queries for events related to thread1 (gets the configure, bob participant, and chats to bob) - // alice writes more messages to both bob and carol in their respective threads - // alice queries for events beyond the latest from the last query, retrieving the additional messages to bob - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); - const carol = await TestDataGenerator.generateDidKeyPersona(); - - const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ - author : alice, - protocolDefinition : { ...threadProtocol } - }); - const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); - expect(protocolConfigureReply.status.code).to.equal(202); - const protocol = protocolConfigure.message.descriptor.definition.protocol; - - // alice creates 2 threads - const thread1 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocol, - protocolPath : 'thread', - }); - const thread1Reply = await dwn.processMessage(alice.did, thread1.message, { dataStream: thread1.dataStream }); - expect(thread1Reply.status.code).to.equal(202); - - const thread2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocol, - protocolPath : 'thread', - }); - const thread2Reply = await dwn.processMessage(alice.did, thread2.message, { dataStream: thread2.dataStream }); - expect(thread2Reply.status.code).to.equal(202); - - // alice adds bob as a participant to thread 1 - const bobParticipant = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : bob.did, - parentContextId : thread1.message.contextId, - protocol : protocol, - protocolPath : 'thread/participant' - }); - const bobParticipantReply = await dwn.processMessage(alice.did, bobParticipant.message, { dataStream: bobParticipant.dataStream }); - expect(bobParticipantReply.status.code).to.equal(202); - - // alice adds carol as a participant to thread 1 - const carolParticipant = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : carol.did, - parentContextId : thread2.message.contextId, - protocol : protocol, - protocolPath : 'thread/participant' - }); - const carolParticipantReply = await dwn.processMessage(alice.did, carolParticipant.message, { dataStream: carolParticipant.dataStream }); - expect(carolParticipantReply.status.code).to.equal(202); - - // alice writes a message to bob on thread 1 - const thread1Chat1 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : bob.did, - parentContextId : thread1.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - }); - const thread1Chat1Reply = await dwn.processMessage(alice.did, thread1Chat1.message, { dataStream: thread1Chat1.dataStream }); - expect(thread1Chat1Reply.status.code).to.equal(202); - - // alice writes a message to carol on thread 2 - const thread2Chat1 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : carol.did, - parentContextId : thread2.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - }); - const thread2Chat1Reply = await dwn.processMessage(alice.did, thread2Chat1.message, { dataStream: thread2Chat1.dataStream }); - expect(thread2Chat1Reply.status.code).to.equal(202); - - // alice writes another message to bob on thread 1 - const thread1Chat2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : bob.did, - parentContextId : thread1.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - }); - const chatMessage2Reply = await dwn.processMessage(alice.did, thread1Chat2.message, { dataStream: thread1Chat2.dataStream }); - expect(chatMessage2Reply.status.code).to.equal(202); + expect(proto2EventsReply.entries?.length).equals(2); // delete, revoke + expect(proto2EventsReply.entries).to.have.members([ + await Message.getCid(deleteProto2Message.message), + await Message.getCid(revokeProto2.recordsWrite.message), + ]); - // alice queries events for thread1 - let threadContextQuery = await TestDataGenerator.generateEventsQuery({ + // query for proto1 events again after the curser, should get nothing + proto1EventsQuery = await TestDataGenerator.generateEventsQuery({ author : alice, - filters : [{ - protocol : protocol, - contextId : thread1.message.contextId, - }], - }); - let threadContextQueryReply = await dwn.processMessage(alice.did, threadContextQuery.message); - expect(threadContextQueryReply.status.code).to.equal(200); - expect(threadContextQueryReply.entries?.length).to.equal(4); - expect(threadContextQueryReply.entries![0]).to.equal(await Message.getCid(thread1.message)); - expect(threadContextQueryReply.entries![1]).to.equal(await Message.getCid(bobParticipant.message)); - expect(threadContextQueryReply.entries![2]).to.equal(await Message.getCid(thread1Chat1.message)); - expect(threadContextQueryReply.entries![3]).to.equal(await Message.getCid(thread1Chat2.message)); - - // alice adds more chats to both threads - const thread1Chat3 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : bob.did, - parentContextId : thread1.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - }); - const thread1Chat3Reply = await dwn.processMessage(alice.did, thread1Chat3.message, { dataStream: thread1Chat3.dataStream }); - expect(thread1Chat3Reply.status.code).to.equal(202); - - const thread2Chat2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : carol.did, - parentContextId : thread2.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', + filters : [{ protocol: proto1 }], + cursor : proto1EventsReply.cursor, }); - const thread2Chat2Reply = await dwn.processMessage(alice.did, thread2Chat2.message, { dataStream: thread2Chat2.dataStream }); - expect(thread2Chat2Reply.status.code).to.equal(202); + proto1EventsReply = await dwn.processMessage(alice.did, proto1EventsQuery.message); + expect(proto1EventsReply.status.code).equals(200); + expect(proto1EventsReply.entries?.length).equals(0); - // query beyond a cursor - threadContextQuery = await TestDataGenerator.generateEventsQuery({ + // query for proto2 events again after the curser, should get nothing + proto2EventsQuery = await TestDataGenerator.generateEventsQuery({ author : alice, - filters : [{ - protocol : protocol, - contextId : thread1.message.contextId, - }], - cursor: threadContextQueryReply.cursor, + filters : [{ protocol: proto2 }], + cursor : proto2EventsReply.cursor, }); - threadContextQueryReply = await dwn.processMessage(alice.did, threadContextQuery.message); - expect(threadContextQueryReply.status.code).to.equal(200); - expect(threadContextQueryReply.entries?.length).to.equal(1); - expect(threadContextQueryReply.entries![0]).to.equal(await Message.getCid(thread1Chat3.message)); + proto2EventsReply = await dwn.processMessage(alice.did, proto2EventsQuery.message); + expect(proto2EventsReply.status.code).equals(200); + expect(proto2EventsReply.entries?.length).equals(0); }); }); }; \ No newline at end of file diff --git a/tests/scenarios/subscriptions.spec.ts b/tests/scenarios/subscriptions.spec.ts index 462e6f45f..2643d9988 100644 --- a/tests/scenarios/subscriptions.spec.ts +++ b/tests/scenarios/subscriptions.spec.ts @@ -17,7 +17,7 @@ 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'; -import { Dwn, DwnConstant, DwnInterfaceName, DwnMethodName, Message } from '../../src/index.js'; +import { Dwn, DwnInterfaceName, DwnMethodName, Message } from '../../src/index.js'; import { expect } from 'chai'; @@ -421,632 +421,6 @@ export function testSubscriptionScenarios(): void { }); }); - it('filters by protocol & contextId across multiple protocolPaths', async () => { - // scenario: subscribe to multiple protocolPaths for a given protocol and parentId - // alice installs a protocol and creates a thread - // alice subscribes to with 3 filters: the thread itself, the thread/participants as well as thread thread/chats - // alice adds bob and carol as participants to the thread - // alice, bob, and carol all create messages - // alice deletes carol participant message - // alice checks that the correct messages were omitted - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); - const carol = await TestDataGenerator.generateDidKeyPersona(); - - // create protocol - const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ - author : alice, - protocolDefinition : { ...threadProtocol } - }); - const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); - expect(protocolConfigureReply.status.code).to.equal(202); - const protocol = protocolConfigure.message.descriptor.definition.protocol; - - // alice creates thread - const thread = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocol, - protocolPath : 'thread' - }); - const threadReply = await dwn.processMessage(alice.did, thread.message, { dataStream: thread.dataStream }); - expect(threadReply.status.code).to.equal(202); - - // subscribe to this thread's events - const messages:string[] = []; - const subscriptionHandler = async (event: MessageEvent):Promise => { - const { message } = event; - messages.push(await Message.getCid(message)); - }; - - const threadSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [ - { protocol: protocol, protocolPath: 'thread', contextId: thread.message.contextId }, // thread updates - { protocol: protocol, protocolPath: 'thread/participant', contextId: thread.message.contextId }, // participant updates - { protocol: protocol, protocolPath: 'thread/chat', contextId: thread.message.contextId } // chat updates - ], - }); - const threadSubscriptionReply = await dwn.processMessage(alice.did, threadSubscription.message, { - subscriptionHandler - }); - expect(threadSubscriptionReply.status.code).to.equal(200); - expect(threadSubscriptionReply.subscription).to.exist; - - // add another thread as a control, will not show up in handled events - // NOTE: we purposely create this thread early in the test to ensure an external pub/sub system can have ample time to process the message - const additionalThread = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocol, - protocolPath : 'thread' - }); - const additionalThreadReply = await dwn.processMessage(alice.did, additionalThread.message, { dataStream: additionalThread.dataStream }); - expect(additionalThreadReply.status.code).to.equal(202); - - // add bob as participant - const bobParticipant = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : bob.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/participant' - }); - const bobParticipantReply = await dwn.processMessage(alice.did, bobParticipant.message, { dataStream: bobParticipant.dataStream }); - expect(bobParticipantReply.status.code).to.equal(202); - - // add carol as participant - const carolParticipant = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : carol.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/participant' - }); - const carolParticipantReply = await dwn.processMessage(alice.did, carolParticipant.message, { dataStream: carolParticipant.dataStream }); - expect(carolParticipantReply.status.code).to.equal(202); - - // poll until we have the 2 participant messages - await Poller.pollUntilSuccessOrTimeout(async () => { - // the messages array should have the two participant messages - expect(messages.length).to.equal(2); - expect(messages).to.have.members([ - await Message.getCid(bobParticipant.message), - await Message.getCid(carolParticipant.message), - ]); - }); - - // bob adds two chat messages - const message1FromBob = await TestDataGenerator.generateRecordsWrite({ - author : bob, - recipient : alice.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - protocolRole : 'thread/participant', - }); - const message1FromBobReply = await dwn.processMessage(alice.did, message1FromBob.message, { dataStream: message1FromBob.dataStream }); - expect(message1FromBobReply.status.code).to.equal(202); - - const message2FromBob = await TestDataGenerator.generateRecordsWrite({ - author : bob, - recipient : alice.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - protocolRole : 'thread/participant', - }); - const message2FromBobReply = await dwn.processMessage(alice.did, message2FromBob.message, { dataStream: message2FromBob.dataStream }); - expect(message2FromBobReply.status.code).to.equal(202); - - const messageFromCarol = await TestDataGenerator.generateRecordsWrite({ - author : carol, - recipient : alice.did, - parentContextId : thread.message.contextId, - protocol : protocol, - protocolPath : 'thread/chat', - protocolRole : 'thread/participant', - }); - const messageFromCarolReply = await dwn.processMessage(alice.did, messageFromCarol.message, { dataStream: messageFromCarol.dataStream }); - expect(messageFromCarolReply.status.code).to.equal(202); - - await Poller.pollUntilSuccessOrTimeout(async () => { - // should have the 3 chat messages - expect(messages.length).to.equal(5); - expect(messages).to.include.members([ - await Message.getCid(message1FromBob.message), - await Message.getCid(message2FromBob.message), - await Message.getCid(messageFromCarol.message), - ]); - }); - - // delete carol participant - const deleteCarol = await TestDataGenerator.generateRecordsDelete({ - author : alice, - recordId : carolParticipant.message.recordId - }); - const deleteCarolReply = await dwn.processMessage(alice.did, deleteCarol.message); - expect(deleteCarolReply.status.code).to.equal(202); - - await Poller.pollUntilSuccessOrTimeout(async () => { - // should have the delete of carol as a participant - expect(messages.length).to.equal(6); - expect(messages).to.include.members([ - await Message.getCid(deleteCarol.message) - ]); - - // although we know the control thread is not in the messages array due to the counts - // we explicitly check that it is not in the array as a defensive measure - // NOTE: we purposely check down here to give the message ample time to be processed - expect(messages).to.not.include(await Message.getCid(additionalThread.message)); - }); - }); - - it('filters by schema', async () => { - //SCENARIO: - // alice creates 2 subscriptions, one for schema1 and one for schema2 - // alice creates a record each for schema1 and schema2 - // alice checks that the appropriate messages were received by their respective handlers - // alice updates the record for schema1 - // alice deletes the record for schema2 - // alice checks that the appropriate messages were received by their respective handlers - - const alice = await TestDataGenerator.generateDidKeyPersona(); - - // we will add messageCids to these arrays as they are received by their handler to check against later - const schema1Messages:string[] = []; - const schema2Messages:string[] = []; - - // we add a handler to the subscription and add the messageCid to the appropriate array - const schema1Handler = async (event: MessageEvent):Promise => { - const { message } = event; - const messageCid = await Message.getCid(message); - schema1Messages.push(messageCid); - }; - - // subscribe to schema1 messages - const schema1Subscription = await TestDataGenerator.generateEventsSubscribe({ author: alice, filters: [{ schema: 'http://schema1' }] }); - const schema1SubscriptionReply = await dwn.processMessage(alice.did, schema1Subscription.message, { subscriptionHandler: schema1Handler }); - expect(schema1SubscriptionReply.status.code).to.equal(200); - expect(schema1SubscriptionReply.subscription?.id).to.equal(await Message.getCid(schema1Subscription.message)); - - // we add a handler to the subscription and add the messageCid to the appropriate array - const schema2Handler = async (event: MessageEvent):Promise => { - const { message } = event; - const messageCid = await Message.getCid(message); - schema2Messages.push(messageCid); - }; - - // subscribe to schema2 messages - const schema2Subscription = await TestDataGenerator.generateEventsSubscribe({ author: alice, filters: [{ schema: 'http://schema2' }] }); - const schema2SubscriptionReply = await dwn.processMessage(alice.did, schema2Subscription.message, { subscriptionHandler: schema2Handler }); - expect(schema2SubscriptionReply.status.code).to.equal(200); - expect(schema2SubscriptionReply.subscription?.id).to.equal(await Message.getCid(schema2Subscription.message)); - - // create some random record, will not show up in records subscription - const write1Random = await TestDataGenerator.generateRecordsWrite({ author: alice }); - const write1RandomResponse = await dwn.processMessage(alice.did, write1Random.message, { dataStream: write1Random.dataStream }); - expect(write1RandomResponse.status.code).to.equal(202); - - // create a record for schema1 - const write1schema1 = await TestDataGenerator.generateRecordsWrite({ author: alice, schema: 'http://schema1' }); - const write1Response = await dwn.processMessage(alice.did, write1schema1.message, { dataStream: write1schema1.dataStream }); - expect(write1Response.status.code).equals(202); - - // create a record for schema2 - const write1schema2 = await TestDataGenerator.generateRecordsWrite({ author: alice, schema: 'http://schema2' }); - const write1Proto2Response = await dwn.processMessage(alice.did, write1schema2.message, { dataStream: write1schema2.dataStream }); - expect(write1Proto2Response.status.code).equals(202); - - // poll until the messages are received by the handlers - await Poller.pollUntilSuccessOrTimeout(async () => { - // schema1 messages from handler has the new message representing the write. - expect(schema1Messages.length).to.equal(1, 'schema1'); - expect(schema1Messages).to.include(await Message.getCid(write1schema1.message)); - - // schema2 messages from handler has the new message representing the write. - expect(schema2Messages.length).to.equal(1, 'schema2'); - expect(schema2Messages).to.include(await Message.getCid(write1schema2.message)); - }); - - // create update the record for schema1 - const update1schema1 = await TestDataGenerator.generateFromRecordsWrite({ author: alice, existingWrite: write1schema1.recordsWrite }); - const update1Response = await dwn.processMessage(alice.did, update1schema1.message, { dataStream: update1schema1.dataStream }); - expect(update1Response.status.code).equals(202); - - // delete the record for schema2 - const deleteschema2 = await TestDataGenerator.generateRecordsDelete({ author: alice, recordId: write1schema2.message.recordId }); - const deleteSchema2Response = await dwn.processMessage(alice.did, deleteschema2.message); - expect(deleteSchema2Response.status.code).equals(202); - - await Poller.pollUntilSuccessOrTimeout(async () => { - // schema1 messages from handler has the new message representing the update. - expect(schema1Messages.length).to.equal(2, 'schema1'); - expect(schema1Messages).to.include(await Message.getCid(update1schema1.message)); - - // schema2 messages from handler has the new message representing the delete. - expect(schema2Messages.length).to.equal(2, 'schema2'); - expect(schema2Messages).to.include(await Message.getCid(deleteschema2.message)); - }); - }); - - it('filters by recordId', async () => { - // alice creates a 2 record and don't process them yet. - // alice creates a subscription for only one of the recordIds - // alice now process both records - // alice updates the record that was subscribed to - // check that the subscription handler has both the write and update messages - // delete both records - // check that the subscription handler has the delete message for the subscribed recordId - - const alice = await TestDataGenerator.generateDidKeyPersona(); - - // create 2 records - const write1 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema1' - }); - - const write2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - schema : 'schema1' - }); - - // create a subscription and capture the messages associated with the recordId for write1 - const messages: string[] = []; - const subscriptionHandler = async (event: MessageEvent):Promise => { - const { message } = event; - messages.push(await Message.getCid(message)); - }; - - const recordIdSubscribe = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ recordId: write1.message.recordId }] - }); - const recordIdSubscribeReply = await dwn.processMessage(alice.did, recordIdSubscribe.message, { - subscriptionHandler - }); - expect(recordIdSubscribeReply.status.code).to.equal(200); - - // process both records - const write1Reply = await dwn.processMessage(alice.did, write1.message, { dataStream: write1.dataStream }); - expect(write1Reply.status.code).to.equal(202); - const write2Reply = await dwn.processMessage(alice.did, write2.message, { dataStream: write2.dataStream }); - expect(write2Reply.status.code).to.equal(202); - - // update the subscribed record - const update = await TestDataGenerator.generateFromRecordsWrite({ - author : alice, - existingWrite : write1.recordsWrite, - }); - const updateReply = await dwn.processMessage(alice.did, update.message, { dataStream: update.dataStream }); - expect(updateReply.status.code).to.equal(202); - - // check that the subscription handler has both the write and update messages - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(messages.length).to.equal(2); - expect(messages).to.have.members([ - await Message.getCid(write1.message), - await Message.getCid(update.message) - ]); - }); - - // delete both records - const deleteWrite1 = await TestDataGenerator.generateRecordsDelete({ - author : alice, - recordId : write1.message.recordId, - }); - const deleteWrite1Reply = await dwn.processMessage(alice.did, deleteWrite1.message); - expect(deleteWrite1Reply.status.code).to.equal(202); - - const deleteWrite2 = await TestDataGenerator.generateRecordsDelete({ - author : alice, - recordId : write2.message.recordId, - }); - const deleteWrite2Reply = await dwn.processMessage(alice.did, deleteWrite2.message); - expect(deleteWrite2Reply.status.code).to.equal(202); - - // check that the subscription handler has the delete message for the subscribed recordId - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(messages.length).to.equal(3); // write1, update, delete - expect(messages).to.include(await Message.getCid(deleteWrite1.message)); - - // confirm that messages for write2 is not in the messages array - expect(messages).to.not.include(await Message.getCid(write2.message)); - expect(messages).to.not.include(await Message.getCid(deleteWrite2.message)); - }); - }); - - it('filters by recipient', async () => { - // scenario: - // alice subscribes to messages with herself as the recipient - // bob and carol sends a messages to alice - // alice sends a message to bob - // bob and carol send a messages to each other - // alice checks that the receivedMessages array only contains the messages from bob and carol to alice - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); - const carol = 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 protocol = protocolConfigure.message.descriptor.definition.protocol; - - // create a control handler to capture ALL messages in the protocol - // this will be used to confirm that the messages are not received by alice - // we do this to test that messages are not received by one handler but have had enough time to be processed - // in some external pub/sub system it may take time for the message to be processed - const allMessages:string[] = []; - const allHandler = async (event: MessageEvent): Promise => { - const { message } = event; - allMessages.push(await Message.getCid(message)); - }; - const allSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ protocol: protocol }] - }); - const allSubscriptionReply = await dwn.processMessage(alice.did, allSubscription.message, { subscriptionHandler: allHandler }); - expect(allSubscriptionReply.status.code).to.equal(200); - - // alice subscribes to messages with herself as the recipient on her own DWN - const aliceMessages:string[] = []; - const handler = async (event: MessageEvent): Promise => { - const { message } = event; - const messageCid = await Message.getCid(message); - aliceMessages.push(messageCid); - }; - - const recipientSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ recipient: alice.did }] - }); - const authorQueryReply = await dwn.processMessage(alice.did, recipientSubscription.message, { subscriptionHandler: handler }); - expect(authorQueryReply.status.code).to.equal(200); - - - // common properties for the post messages - const postProperties = { - protocol : protocol, - protocolPath : 'post', - schema : freeForAll.types.post.schema, - dataFormat : freeForAll.types.post.dataFormats[0], - }; - - // bob sends a message to alice - const messageFromBobToAlice = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : bob, - recipient : alice.did, - }); - const messageFromBobToAliceReply = - await dwn.processMessage(alice.did, messageFromBobToAlice.message, { dataStream: messageFromBobToAlice.dataStream }); - expect(messageFromBobToAliceReply.status.code).to.equal(202); - - // carol sends a message to alice - const messageFromCarolToAlice = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : carol, - recipient : alice.did, - }); - const messageFromCarolToAliceReply = - await dwn.processMessage(alice.did, messageFromCarolToAlice.message, { dataStream: messageFromCarolToAlice.dataStream }); - expect(messageFromCarolToAliceReply.status.code).to.equal(202); - - // alice sends a message to bob, this will not show up in the aliceMessages array, but will in the allMessages array - const messageFromAliceToBob = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : alice, - recipient : bob.did, - }); - const messageFromAliceToBobReply = - await dwn.processMessage(alice.did, messageFromAliceToBob.message, { dataStream: messageFromAliceToBob.dataStream }); - expect(messageFromAliceToBobReply.status.code).to.equal(202); - - // bob sends a message to carol, this will not show up in the aliceMessages array, but will in the allMessages array - const messageFromBobToCarol = await TestDataGenerator.generateRecordsWrite({ - ...postProperties, - author : bob, - recipient : carol.did, - }); - const messageFromBobToCarolReply = - await dwn.processMessage(alice.did, messageFromBobToCarol.message, { dataStream: messageFromBobToCarol.dataStream }); - expect(messageFromBobToCarolReply.status.code).to.equal(202); - - // poll until the messages are received by the handlers - await Poller.pollUntilSuccessOrTimeout(async () => { - // check that the aliceMessages array only contains the messages from bob and carol to alice - expect(aliceMessages.length).to.equal(2); - expect(aliceMessages).to.have.members([ - await Message.getCid(messageFromBobToAlice.message), - await Message.getCid(messageFromCarolToAlice.message) - ]); - - // check that the allMessages array contains all the messages - expect(allMessages.length).to.equal(4); - expect(allMessages).to.have.members([ - await Message.getCid(messageFromBobToAlice.message), - await Message.getCid(messageFromCarolToAlice.message), - await Message.getCid(messageFromAliceToBob.message), - await Message.getCid(messageFromBobToCarol.message) - ]); - }); - }); - - it('filters by dataFormat', async () => { - // Scenario: Alice subscribes events relating to `image/jpeg` after which a number of record messages of various data formats are processed - // 1. Alice subscribes for `image/jpeg` records - // 2. Alice creates 3 files, one of them `image/jpeg` - // 3. Alice receives the one `image/jpeg` message - // 4. Alice adds another image - // 5. Alice receives the other `image/jpeg` message - - const alice = await TestDataGenerator.generateDidKeyPersona(); - - const imageMessages: string[] = []; - const imageHandler = async (event: MessageEvent):Promise => { - const { message } = event; - imageMessages.push(await Message.getCid(message)); - }; - - // alice subscribes to image/jpeg changes - const imageSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ dataFormat: 'image/jpeg' }] - }); - const imageSubscriptionReply = await dwn.processMessage(alice.did, imageSubscription.message, { - subscriptionHandler: imageHandler - }); - expect(imageSubscriptionReply.status.code).to.equal(200); - - // NOTE: we purposely create the non-matching files ahead of the matching files. - // this helps ensure that the non-matching files have had ample time to be processed by an external pub/sub system - - // write an `application/text` file (will not match) - const textFile = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'application/text' - }); - const textFileReply = await dwn.processMessage(alice.did, textFile.message, { dataStream: textFile.dataStream }); - expect(textFileReply.status.code).to.equal(202); - - // write an `application/json` file (will not match) - const jsonData = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'application/json' - }); - const jsonDataReply = await dwn.processMessage(alice.did, jsonData.message, { dataStream: jsonData.dataStream }); - expect(jsonDataReply.status.code).to.equal(202); - - // write an `image/jpeg` file (will match) - const imageData = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'image/jpeg' - }); - const imageDataReply = await dwn.processMessage(alice.did, imageData.message, { dataStream: imageData.dataStream }); - expect(imageDataReply.status.code).to.equal(202); - - // poll until the image message is received by the handlers - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(imageMessages.length).to.equal(1); - expect(imageMessages).to.have.members([ await Message.getCid(imageData.message) ]); - }); - - // add another `image/jpeg` file, this should also be received by the handler - const imageData2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - dataFormat : 'image/jpeg' - }); - const imageData2Reply = await dwn.processMessage(alice.did, imageData2.message, { dataStream: imageData2.dataStream }); - expect(imageData2Reply.status.code).to.equal(202); - - // poll until the image message is received by the handlers - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(imageMessages.length).to.equal(2); - expect(imageMessages).to.include.members([ await Message.getCid(imageData2.message) ]); - }); - });; - - it('filters by dataSize', async () => { - // scenario: - // alice subscribes to messages with data size under a threshold - // alice also subscribes to messages with data size over a threshold - // alice inserts both small and large data messages - // alice checks that the messages were received by their respective handlers - - const alice = await TestDataGenerator.generateDidKeyPersona(); - - // subscribe to small data size messages - const smallMessages: string[] = []; - const subscriptionHandler = async (event: MessageEvent):Promise => { - const { message } = event; - smallMessages.push(await Message.getCid(message)); - }; - const smallMessageSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ dataSize: { lte: DwnConstant.maxDataSizeAllowedToBeEncoded } }] - }); - const smallMessageSubscriptionReply = await dwn.processMessage(alice.did, smallMessageSubscription.message, { - subscriptionHandler, - }); - expect(smallMessageSubscriptionReply.status.code).to.equal(200); - - // subscribe to large data size messages - const largeMessages: string[] = []; - const largeSubscriptionHandler = async (event: MessageEvent):Promise => { - const { message } = event; - largeMessages.push(await Message.getCid(message)); - }; - const largeMessageSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ dataSize: { gt: DwnConstant.maxDataSizeAllowedToBeEncoded } }] - }); - const largeMessageSubscriptionReply = await dwn.processMessage(alice.did, largeMessageSubscription.message, { - subscriptionHandler: largeSubscriptionHandler, - }); - expect(largeMessageSubscriptionReply.status.code).to.equal(200); - - // add a small data size record - const smallSize1 = await TestDataGenerator.generateRecordsWrite({ - author: alice, - }); - const smallSize1Reply = await dwn.processMessage(alice.did, smallSize1.message, { dataStream: smallSize1.dataStream }); - expect(smallSize1Reply.status.code).to.equal(202); - - // add a large data size record - const largeSize = await TestDataGenerator.generateRecordsWrite({ - author : alice, - data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded + 1) - }); - const largeSizeReply = await dwn.processMessage(alice.did, largeSize.message, { dataStream: largeSize.dataStream }); - expect(largeSizeReply.status.code).to.equal(202); - - await Poller.pollUntilSuccessOrTimeout(async () => { - // smallMessages array should only contain the small data size record - expect(smallMessages.length).to.equal(1); - expect(smallMessages).to.have.members([ await Message.getCid(smallSize1.message) ]); - - // largeMessages array should only contain the large data size record - expect(largeMessages.length).to.equal(1); - expect(largeMessages).to.have.members([ await Message.getCid(largeSize.message) ]); - }); - - // add another large record - const largeSize2 = await TestDataGenerator.generateRecordsWrite({ - author : alice, - data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded + 1) - }); - const largeSize2Reply = await dwn.processMessage(alice.did, largeSize2.message, { dataStream: largeSize2.dataStream }); - expect(largeSize2Reply.status.code).to.equal(202); - - // add another small record - const smallSize2 = await TestDataGenerator.generateRecordsWrite({ - author: alice, - }); - const smallSize2Reply = await dwn.processMessage(alice.did, smallSize2.message, { dataStream: smallSize2.dataStream }); - expect(smallSize2Reply.status.code).to.equal(202); - - await Poller.pollUntilSuccessOrTimeout(async () => { - // smallMessages array should only contain the two small data size records - expect(smallMessages.length).to.equal(2); - expect(smallMessages).to.include.members([ - await Message.getCid(smallSize1.message), - await Message.getCid(smallSize2.message) - ]); - - // largeMessages array should only contain the two large data size records - expect(largeMessages.length).to.equal(2); - expect(largeMessages).to.include.members([ - await Message.getCid(largeSize.message), - await Message.getCid(largeSize2.message) - ]); - }); - }); - it('does not emit events after subscription is closed', async () => { // scenario: create two subscriptions. // write a message, check that both subscriptions receive the message. @@ -1147,8 +521,7 @@ export function testSubscriptionScenarios(): void { allMessages.push(await Message.getCid(message)); }; const allSubscription = await TestDataGenerator.generateEventsSubscribe({ - author : alice, - filters : [{ schema: 'http://schema1' }] + author: alice, }); const allSubscriptionReply = await dwn.processMessage(alice.did, allSubscription.message, { subscriptionHandler: allHandler }); expect(allSubscriptionReply.status.code).to.equal(200); diff --git a/tests/utils/events.spec.ts b/tests/utils/events.spec.ts new file mode 100644 index 000000000..af13622ee --- /dev/null +++ b/tests/utils/events.spec.ts @@ -0,0 +1,120 @@ +import type { EventsFilter } from '../../src/types/events-types.js'; +import type { Filter } from '../../src/types/query-types.js'; + +import { Events } from '../../src/utils/events.js'; +import { FilterUtility } from '../../src/utils/filter.js'; +import { DwnInterfaceName, DwnMethodName, PermissionsProtocol, TestDataGenerator } from '../../src/index.js'; + +import sinon from 'sinon'; + +import chaiAsPromised from 'chai-as-promised'; +import chai, { expect } from 'chai'; + + +chai.use(chaiAsPromised); + +describe('Events Utils', () => { + + after(() => { + sinon.restore(); + }); + + beforeEach(() => { + sinon.restore(); + }); + + describe('constructPermissionRecordsFilter', () => { + it('does not apply any tag filters to non-protocol-filtered events', async () => { + const eventsFilter: EventsFilter = { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write + }; + + const messageFilter: Filter[] = Events.convertFilters([eventsFilter]); + expect(messageFilter.length).to.equal(1); + expect(messageFilter[0].interface).to.equal(DwnInterfaceName.Records); + expect(messageFilter[0].method).to.deep.equal(DwnMethodName.Write); + }); + + it('applies appropriate tag filters to protocol-filtered events', async () => { + // in order to filter for protocol-specific permission requests, grants and revocations we add a a protocol tag index to the message + // when we filter for a protocol, we should add the tag filters in to accommodate for the protocol tag index + + const exampleProtocol = 'https://example.xyz/protocol/1'; + + // only a protocol filter is applied + const protocolEventsFilter: EventsFilter = { + protocol: exampleProtocol, + }; + + // here we are testing where only a protocol EventsFilter is applied + // we should expect the EventsFilter to be split into two MessageStore Filters + // the first filter should be the protocol tag filter applied to the permissions protocol uri + // the second filter should be the remaining filter, only containing a protocol filter to the protocol we are targeting + const protocolMessageFilter: Filter[] = Events.convertFilters([protocolEventsFilter]); + expect(protocolMessageFilter.length).to.equal(2); + + const permissionRecordsFilter = protocolMessageFilter[0]; + // should have two filter properties: protocol tag filter and a protocol filter for the permissions protocol + expect(Object.keys(permissionRecordsFilter).length).to.equal(2); + expect(permissionRecordsFilter['tag.protocol']).to.equal(exampleProtocol); + expect(permissionRecordsFilter.protocol).to.equal(PermissionsProtocol.uri); + + // should only have a protocol filter for the targeted protocol + const remainingFilter = protocolMessageFilter[1]; + expect(Object.keys(remainingFilter).length).to.equal(1); + expect(remainingFilter.protocol).to.equal(exampleProtocol); + + + // with other filters in addition to the filtered protocol + const otherEventsFilter: EventsFilter = { + protocol : exampleProtocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write + }; + + const messageFilter: Filter[] = Events.convertFilters([otherEventsFilter]); + expect(messageFilter.length).to.equal(2); + + const protocolTagFilter2 = messageFilter[0]; + // should have two filter properties: protocol tag filter and a protocol filter for the permissions protocol + expect(Object.keys(protocolTagFilter2).length).to.equal(2); + expect(permissionRecordsFilter['tag.protocol']).to.equal(exampleProtocol); + expect(permissionRecordsFilter.protocol).to.equal(PermissionsProtocol.uri); + + const remainingFilter2 = messageFilter[1]; + // should have the remaining filters + expect(Object.keys(remainingFilter2).length).to.equal(3); + expect(remainingFilter2.protocol).to.equal(exampleProtocol); + expect(remainingFilter2.interface).to.equal(DwnInterfaceName.Records); + expect(remainingFilter2.method).to.deep.equal(DwnMethodName.Write); + }); + + it('applies appropriate tag filters to protocol-filtered events with messageTimestamp filter', async () => { + // should apply the dateUpdated filter to the protocol tag filter + + const exampleProtocol = 'https://example.xyz/protocol/1'; + const dateUpdatedTimestamp = TestDataGenerator.randomTimestamp(); + const messageTimestampFilterResult = FilterUtility.convertRangeCriterion({ from: dateUpdatedTimestamp }); + + const withDateUpdatedFilter: EventsFilter = { + protocol : exampleProtocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + messageTimestamp : { from: dateUpdatedTimestamp } + }; + + const messageFilter: Filter[] = Events.convertFilters([withDateUpdatedFilter]); + expect(messageFilter.length).to.equal(2); + expect(messageFilter[0].protocol).to.equal(PermissionsProtocol.uri); + expect(messageFilter[0]['tag.protocol']).to.equal(exampleProtocol); + expect(messageFilter[0].messageTimestamp).to.deep.equal(messageTimestampFilterResult); + + + expect(messageFilter[1].protocol).to.equal(exampleProtocol); + expect(messageFilter[1].interface).to.equal(DwnInterfaceName.Records); + expect(messageFilter[1].method).to.deep.equal(DwnMethodName.Write); + expect(messageFilter[1].messageTimestamp).to.deep.equal(messageTimestampFilterResult); + }); + }); +}); \ No newline at end of file