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