Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poll model #3036

Merged
merged 30 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5ca533c
first cut poll model
Dec 30, 2022
984e5eb
process incoming poll relations
Dec 30, 2022
e578d84
allow alt event types in relations model
Jan 4, 2023
1123f1d
allow alt event types in relations model
Jan 4, 2023
ea71efe
remove unneccesary checks on remove relation
Jan 4, 2023
dfe4038
comment
Jan 4, 2023
515db7a
Revert "allow alt event types in relations model"
Jan 4, 2023
ae6ffb7
Revert "Revert "allow alt event types in relations model""
Jan 4, 2023
f189901
Merge branch 'psg-1014/relations-alt-event-types' into psg-1014/poll-…
Jan 4, 2023
cace5d4
basic handling for new poll relations
Jan 4, 2023
6b4f630
Merge branch 'develop' into psg-1014/poll-model
Jan 5, 2023
0f7829a
tests
Jan 9, 2023
32abfed
test room.processPollEvents
Jan 9, 2023
797123c
join processBeaconEvents and poll events in client
Jan 9, 2023
51896e1
Merge branch 'develop' into psg-1014/poll-model
Jan 9, 2023
ad49a00
Merge branch 'develop' into psg-1014/poll-model
Jan 10, 2023
b158dcc
tidy and set 23 copyrights
Jan 11, 2023
924a6c0
use rooms instance of matrixClient
Jan 11, 2023
d09700f
tidy
Jan 11, 2023
9e0d95e
more copyright
Jan 11, 2023
b173130
simplify processPollEvent code
Jan 12, 2023
15dd1c4
Merge branch 'develop' into psg-1014/poll-model
Jan 12, 2023
4a0494c
throw when poll start event has no roomId
Jan 12, 2023
07d154a
Merge branch 'develop' into psg-1014/poll-model
Jan 15, 2023
68ffea1
updates for events-sdk move
Jan 16, 2023
7d8f51c
more type changes for events-sdk changes
Jan 16, 2023
a6db13b
Merge branch 'develop' into psg-1014/poll-model
Jan 17, 2023
9b94bbd
Merge branch 'develop' into psg-1014/poll-model
Jan 26, 2023
e08f639
comment
Jan 26, 2023
08d6ccf
more comment
Jan 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions spec/unit/models/poll.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { IEvent, MatrixEvent, PollEvent } from "../../../src";
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls";
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
import { Poll } from "../../../src/models/poll";
import { getMockClientWithEventEmitter } from "../../test-utils/client";

jest.useFakeTimers();

describe("Poll", () => {
const mockClient = getMockClientWithEventEmitter({
relations: jest.fn(),
});
const roomId = "!room:server";
// 14.03.2022 16:15
const now = 1647270879403;

const basePollStartEvent = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
});
basePollStartEvent.event.event_id = "$12345";

beforeEach(() => {
jest.clearAllMocks();
jest.setSystemTime(now);

mockClient.relations.mockResolvedValue({ events: [] });
});

let eventId = 1;
const makeRelatedEvent = (eventProps: Partial<IEvent>, timestamp = now): MatrixEvent => {
const event = new MatrixEvent({
...eventProps,
content: {
...(eventProps.content || {}),
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: basePollStartEvent.getId(),
},
},
});
event.event.origin_server_ts = timestamp;
event.event.event_id = `${eventId++}`;
return event;
};

it("initialises with root event", () => {
const poll = new Poll(basePollStartEvent, mockClient);
expect(poll.roomId).toEqual(roomId);
expect(poll.pollId).toEqual(basePollStartEvent.getId());
expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent);
expect(poll.isEnded).toBe(false);
});

it("throws when poll start has no room id", () => {
const pollStartEvent = new MatrixEvent(
PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
);
expect(() => new Poll(pollStartEvent, mockClient)).toThrowError("Invalid poll start event.");
});

it("throws when poll start has no event id", () => {
const pollStartEvent = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
});
expect(() => new Poll(pollStartEvent, mockClient)).toThrowError("Invalid poll start event.");
});

describe("fetching responses", () => {
it("calls relations api and emits", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
const emitSpy = jest.spyOn(poll, "emit");
const responses = await poll.getResponses();
expect(mockClient.relations).toHaveBeenCalledWith(roomId, basePollStartEvent.getId(), "m.reference");
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
});

it("returns existing responses object after initial fetch", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
const responses = await poll.getResponses();
const responses2 = await poll.getResponses();
// only fetched relations once
expect(mockClient.relations).toHaveBeenCalledTimes(1);
// strictly equal
expect(responses).toBe(responses2);
});

it("waits for existing relations request to finish when getting responses", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
const firstResponsePromise = poll.getResponses();
const secondResponsePromise = poll.getResponses();
await firstResponsePromise;
expect(firstResponsePromise).toEqual(secondResponsePromise);
await secondResponsePromise;
expect(mockClient.relations).toHaveBeenCalledTimes(1);
});

it("filters relations for relevent response events", async () => {
const replyEvent = new MatrixEvent({ type: "m.room.message" });
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });

mockClient.relations.mockResolvedValue({
events: [replyEvent, stableResponseEvent, unstableResponseEvent],
});
const poll = new Poll(basePollStartEvent, mockClient);
const responses = await poll.getResponses();
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
});

describe("with poll end event", () => {
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! });
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable! });
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);

beforeEach(() => {
mockClient.relations.mockResolvedValue({
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, stablePollEndEvent],
});
});

it("sets poll end event with stable event type", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
jest.spyOn(poll, "emit");
await poll.getResponses();

expect(poll.isEnded).toBe(true);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});

it("sets poll end event with unstable event type", async () => {
mockClient.relations.mockResolvedValue({
events: [unstablePollEndEvent],
});
const poll = new Poll(basePollStartEvent, mockClient);
jest.spyOn(poll, "emit");
await poll.getResponses();

expect(poll.isEnded).toBe(true);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});

it("filters out responses that were sent after poll end", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
const responses = await poll.getResponses();

// just response type events
// and response with ts after poll end event is excluded
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
});
});
});

describe("onNewRelation()", () => {
it("discards response if poll responses have not been initialised", () => {
const poll = new Poll(basePollStartEvent, mockClient);
jest.spyOn(poll, "emit");
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);

poll.onNewRelation(responseEvent);

// did not add response -> no emit
expect(poll.emit).not.toHaveBeenCalled();
});

it("sets poll end event when responses are not initialised", () => {
const poll = new Poll(basePollStartEvent, mockClient);
jest.spyOn(poll, "emit");
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! });

poll.onNewRelation(stablePollEndEvent);

expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});

it("sets poll end event and refilters responses based on timestamp", async () => {
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! });
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
mockClient.relations.mockResolvedValue({
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd],
});
const poll = new Poll(basePollStartEvent, mockClient);
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");

expect(responses.getRelations().length).toEqual(3);
poll.onNewRelation(stablePollEndEvent);

expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
expect(responses.getRelations().length).toEqual(2);
// after end timestamp event is removed
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
});

it("filters out irrelevant relations", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
// init responses
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
const replyEvent = new MatrixEvent({ type: "m.room.message" });

poll.onNewRelation(replyEvent);

// did not add response -> no emit
expect(poll.emit).not.toHaveBeenCalled();
expect(responses.getRelations().length).toEqual(0);
});

it("adds poll response relations to responses", async () => {
const poll = new Poll(basePollStartEvent, mockClient);
// init responses
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);

poll.onNewRelation(responseEvent);

// did not add response -> no emit
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
expect(responses.getRelations()).toEqual([responseEvent]);
});
});
});
76 changes: 75 additions & 1 deletion spec/unit/room.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,7 @@ limitations under the License.
*/

import { mocked } from "jest-mock";
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, PollStartEvent } from "matrix-events-sdk";
kerryarchibald marked this conversation as resolved.
Show resolved Hide resolved

import * as utils from "../test-utils/test-utils";
import { emitPromise } from "../test-utils/test-utils";
Expand All @@ -37,6 +38,7 @@ import {
MatrixEvent,
MatrixEventEvent,
PendingEventOrdering,
PollEvent,
RelationType,
RoomEvent,
RoomMember,
Expand Down Expand Up @@ -3228,6 +3230,78 @@ describe("Room", function () {
});
});

describe("processPollEvents()", () => {
let room: Room;
let client: MatrixClient;

beforeEach(() => {
client = getMockClientWithEventEmitter({
decryptEventIfNeeded: jest.fn(),
});
room = new Room(roomId, client, userA);
jest.spyOn(room, "emit").mockClear();
});

const makePollStart = (id: string): MatrixEvent => {
const event = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
});
event.event.event_id = id;
return event;
};

it("adds poll models to room state for a poll start event ", async () => {
const pollStartEvent = makePollStart("1");
const events = [pollStartEvent];

await room.processPollEvents(events);
expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(pollStartEvent);
const pollInstance = room.polls.get(pollStartEvent.getId()!);
expect(pollInstance).toBeTruthy();

expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance);
});

it("adds related events to poll models", async () => {
const pollStartEvent = makePollStart("1");
const pollStartEvent2 = makePollStart("2");
const events = [pollStartEvent, pollStartEvent2];
const pollResponseEvent = new MatrixEvent({
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: RelationType.Reference,
event_id: pollStartEvent.getId(),
},
},
});
const messageEvent = new MatrixEvent({
type: "m.room.messsage",
content: {
text: "hello",
},
});

// init poll
await room.processPollEvents(events);

const poll = room.polls.get(pollStartEvent.getId()!)!;
const poll2 = room.polls.get(pollStartEvent2.getId()!)!;
jest.spyOn(poll, "onNewRelation");
jest.spyOn(poll2, "onNewRelation");

await room.processPollEvents([pollResponseEvent, messageEvent]);

// only called for relevant event
expect(poll.onNewRelation).toHaveBeenCalledTimes(1);
expect(poll.onNewRelation).toHaveBeenCalledWith(pollResponseEvent);

// only called on poll with relation
expect(poll2.onNewRelation).not.toHaveBeenCalled();
});
});

describe("findPredecessorRoomId", () => {
let client: MatrixClient | null = null;
beforeEach(() => {
Expand Down
Loading