From 14de16359989bdc992251dd86301e2ce3654ff68 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 14 Apr 2023 16:54:32 +1200 Subject: [PATCH 1/3] unit test paginating /notifications --- spec/unit/matrix-client.spec.ts | 117 ++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 331f80eed32..06dcd73771a 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -50,6 +50,7 @@ import { MatrixScheduler, Method, Room, + EventTimelineSet, } from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; @@ -2743,4 +2744,120 @@ describe("MatrixClient", function () { expect(mockSecretStorage.isStored).toHaveBeenCalledWith("m.megolm_backup.v1"); }); }); + + describe("paginateEventTimeline()", () => { + describe("notifications timeline", () => { + const unsafeNotification = { + actions: ["notify"], + room_id: "__proto__", + event: testUtils.mkMessage({ + user: userId, + room: "!roomId:server.org", + msg: "I am nefarious", + }), + profile_tag: null, + read: true, + ts: 12345, + }; + + const goodNotification = { + actions: ["notify"], + room_id: "!roomId:server.org", + event: testUtils.mkMessage({ + user: userId, + room: "!roomId:server.org", + msg: "I am nice", + }), + profile_tag: null, + read: true, + ts: 12345, + }; + + const highlightNotification = { + actions: ["notify", { set_tweak: "highlight" }], + room_id: "!roomId:server.org", + event: testUtils.mkMessage({ + user: userId, + room: "!roomId:server.org", + msg: "I am highlighted", + }), + profile_tag: null, + read: true, + ts: 12345, + }; + + const setNotifsResponse = (notifications: any[] = []): void => { + const response: HttpLookup = { + method: "GET", + path: "/notifications", + data: { notifications: JSON.parse(JSON.stringify(notifications)) }, + }; + httpLookups = [response]; + }; + + beforeEach(() => { + makeClient(); + + // this is how notif timeline is set up in react-sdk + const notifTimelineSet = new EventTimelineSet(undefined, { + timelineSupport: true, + pendingEvents: false, + }); + notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); + client.setNotifTimelineSet(notifTimelineSet); + + setNotifsResponse(); + }); + + it("should throw when trying to paginate forwards", async () => { + const timeline = client.getNotifTimelineSet()!.getLiveTimeline(); + await expect( + async () => await client.paginateEventTimeline(timeline, { backwards: false }), + ).rejects.toThrow("paginateNotifTimeline can only paginate backwards"); + }); + + it("defaults limit to 30 events", async () => { + jest.spyOn(client.http, "authedRequest"); + const timeline = client.getNotifTimelineSet()!.getLiveTimeline(); + await client.paginateEventTimeline(timeline, { backwards: true }); + + expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Get, "/notifications", { + limit: "30", + only: "highlight", + }); + }); + + it("filters out unsafe notifications", async () => { + setNotifsResponse([unsafeNotification, goodNotification, highlightNotification]); + + const timelineSet = client.getNotifTimelineSet()!; + const timeline = timelineSet.getLiveTimeline(); + await client.paginateEventTimeline(timeline, { backwards: true }); + + // badNotification not added to timeline + const timelineEvents = timeline.getEvents(); + expect(timelineEvents.length).toEqual(2); + }); + + it("sets push actions on events and add to timeline", async () => { + setNotifsResponse([goodNotification, highlightNotification]); + + const timelineSet = client.getNotifTimelineSet()!; + const timeline = timelineSet.getLiveTimeline(); + await client.paginateEventTimeline(timeline, { backwards: true }); + + const [highlightEvent, goodEvent] = timeline.getEvents(); + expect(highlightEvent.getPushActions()).toEqual({ + notify: true, + tweaks: { + highlight: true, + }, + }); + expect(goodEvent.getPushActions()).toEqual({ + notify: true, + tweaks: {}, + }); + }); + }); + }); }); From d3c4230957bcb7764800bab9e1a4389f924dbfa3 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 14 Apr 2023 17:37:13 +1200 Subject: [PATCH 2/3] add push rule to event --- spec/unit/matrix-client.spec.ts | 71 ++++++++++++++++++++++++++++----- spec/unit/pushprocessor.spec.ts | 5 +-- src/client.ts | 26 +++++++++++- src/models/event.ts | 39 ++++++++++++++++-- src/pushprocessor.ts | 23 +++++++++-- 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 06dcd73771a..ddd95355ad7 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -51,6 +51,11 @@ import { Method, Room, EventTimelineSet, + PushRuleActionName, + TweakName, + RuleId, + IPushRule, + ConditionKind, } from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; @@ -2751,7 +2756,7 @@ describe("MatrixClient", function () { actions: ["notify"], room_id: "__proto__", event: testUtils.mkMessage({ - user: userId, + user: "@villain:server.org", room: "!roomId:server.org", msg: "I am nefarious", }), @@ -2762,11 +2767,12 @@ describe("MatrixClient", function () { const goodNotification = { actions: ["notify"], - room_id: "!roomId:server.org", - event: testUtils.mkMessage({ - user: userId, - room: "!roomId:server.org", - msg: "I am nice", + room_id: "!favouriteRoom:server.org", + event: new MatrixEvent({ + sender: "@bob:server.org", + room_id: "!roomId:server.org", + type: "m.call.invite", + content: {}, }), profile_tag: null, read: true, @@ -2774,12 +2780,12 @@ describe("MatrixClient", function () { }; const highlightNotification = { - actions: ["notify", { set_tweak: "highlight" }], + actions: ["notify", { set_tweak: "highlight", value: true }], room_id: "!roomId:server.org", event: testUtils.mkMessage({ - user: userId, + user: "@bob:server.org", room: "!roomId:server.org", - msg: "I am highlighted", + msg: "I am highlighted banana", }), profile_tag: null, read: true, @@ -2795,6 +2801,41 @@ describe("MatrixClient", function () { httpLookups = [response]; }; + const callRule: IPushRule = { + actions: [PushRuleActionName.Notify], + conditions: [ + { + kind: ConditionKind.EventMatch, + key: "type", + pattern: "m.call.invite", + }, + ], + default: true, + enabled: true, + rule_id: ".m.rule.call", + }; + const masterRule: IPushRule = { + actions: [PushRuleActionName.DontNotify], + conditions: [], + default: true, + enabled: false, + rule_id: RuleId.Master, + }; + const bananaRule = { + actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }], + pattern: "banana", + rule_id: "banana", + default: false, + enabled: true, + } as IPushRule; + const pushRules = { + global: { + underride: [callRule], + override: [masterRule], + content: [bananaRule], + }, + }; + beforeEach(() => { makeClient(); @@ -2807,6 +2848,8 @@ describe("MatrixClient", function () { client.setNotifTimelineSet(notifTimelineSet); setNotifsResponse(); + + client.setPushRules(pushRules); }); it("should throw when trying to paginate forwards", async () => { @@ -2839,7 +2882,7 @@ describe("MatrixClient", function () { expect(timelineEvents.length).toEqual(2); }); - it("sets push actions on events and add to timeline", async () => { + it("sets push details on events and add to timeline", async () => { setNotifsResponse([goodNotification, highlightNotification]); const timelineSet = client.getNotifTimelineSet()!; @@ -2853,9 +2896,15 @@ describe("MatrixClient", function () { highlight: true, }, }); + expect(highlightEvent.getPushDetails().rule).toEqual({ + ...bananaRule, + kind: "content", + }); expect(goodEvent.getPushActions()).toEqual({ notify: true, - tweaks: {}, + tweaks: { + highlight: false, + }, }); }); }); diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index 141e3444593..36153e7deea 100644 --- a/spec/unit/pushprocessor.spec.ts +++ b/spec/unit/pushprocessor.spec.ts @@ -1,6 +1,7 @@ import * as utils from "../test-utils/test-utils"; import { IActionsObject, PushProcessor } from "../../src/pushprocessor"; import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src"; +import { mockClientMethodsUser } from "../test-utils/client"; describe("NotificationService", function () { const testUserId = "@ali:matrix.org"; @@ -45,9 +46,7 @@ describe("NotificationService", function () { }, }; }, - credentials: { - userId: testUserId, - }, + ...mockClientMethodsUser(testUserId), supportsIntentionalMentions: () => true, pushRules: { device: {}, diff --git a/src/client.ts b/src/client.ts index 66765b092ee..8e087c79974 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,6 +30,7 @@ import { MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap, + PushDetails, } from "./models/event"; import { StubStore } from "./store/stub"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call"; @@ -5385,11 +5386,28 @@ export class MatrixClient extends TypedEventEmitter; export class MatrixEvent extends TypedEventEmitter { - private pushActions: IActionsObject | null = null; + // applied push rule and action for this event + private pushDetails: PushDetails = {}; private _replacingEvent: MatrixEvent | null = null; private _localRedactionEvent: MatrixEvent | null = null; private _isCancelled = false; @@ -888,7 +895,7 @@ export class MatrixEvent extends TypedEventEmitter & Pick, ev: MatrixEvent): boolean { @@ -732,6 +739,14 @@ export class PushProcessor { * Get the user's push actions for the given event */ public actionsForEvent(ev: MatrixEvent): IActionsObject { + const { actions } = this.pushActionsForEventAndRulesets(ev, this.client.pushRules); + return actions || ({} as IActionsObject); + } + + public actionsAndRuleForEvent(ev: MatrixEvent): { + actions?: IActionsObject; + rule?: IAnnotatedPushRule; + } { return this.pushActionsForEventAndRulesets(ev, this.client.pushRules); } From c356d1e8dc468155cb3d86a8c2cdcd6fe551b1f9 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 17 Apr 2023 10:44:14 +1200 Subject: [PATCH 3/3] 1% more test coverage --- spec/unit/models/event.spec.ts | 92 ++++++++++++++++++++++++++++++++++ src/models/event.ts | 4 +- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 85058a56b83..e4a0576da33 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -17,6 +17,7 @@ limitations under the License. import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { emitPromise } from "../../test-utils/test-utils"; import { Crypto, IEventDecryptionResult } from "../../../src/crypto"; +import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src"; describe("MatrixEvent", () => { it("should create copies of itself", () => { @@ -216,4 +217,95 @@ describe("MatrixEvent", () => { expect(encryptedEvent.replyEventId).toBeUndefined(); }); }); + + describe("push details", () => { + const pushRule = { + actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }], + pattern: "banana", + rule_id: "banana", + kind: "override", + default: false, + enabled: true, + } as IAnnotatedPushRule; + describe("setPushActions()", () => { + it("sets actions on event", () => { + const actions = { notify: false, tweaks: {} }; + const event = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + }, + }); + event.setPushActions(actions); + + expect(event.getPushActions()).toBe(actions); + }); + + it("sets actions to undefined", () => { + const event = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + }, + }); + event.setPushActions(null); + + // undefined is set on state + expect(event.getPushDetails().actions).toBe(undefined); + // but pushActions getter returns null when falsy + expect(event.getPushActions()).toBe(null); + }); + + it("clears existing push rule", () => { + const prevActions = { notify: true, tweaks: { highlight: true } }; + const actions = { notify: false, tweaks: {} }; + const event = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + }, + }); + event.setPushDetails(prevActions, pushRule); + + event.setPushActions(actions); + + // rule is not in event push cache + expect(event.getPushDetails()).toEqual({ actions }); + }); + }); + + describe("setPushDetails()", () => { + it("sets actions and rule on event", () => { + const actions = { notify: false, tweaks: {} }; + const event = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + }, + }); + event.setPushDetails(actions, pushRule); + + expect(event.getPushDetails()).toEqual({ + actions, + rule: pushRule, + }); + }); + it("clears existing push rule", () => { + const prevActions = { notify: true, tweaks: { highlight: true } }; + const actions = { notify: false, tweaks: {} }; + const event = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + }, + }); + event.setPushDetails(prevActions, pushRule); + + event.setPushActions(actions); + + // rule is not in event push cache + expect(event.getPushDetails()).toEqual({ actions }); + }); + }); + }); }); diff --git a/src/models/event.ts b/src/models/event.ts index 5e462fb595b..5c55449a3f5 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1262,12 +1262,14 @@ export class MatrixEvent extends TypedEventEmitter