mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-06-08 15:21:53 +03:00
* Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule * Re-lint after merge
487 lines
22 KiB
TypeScript
487 lines
22 KiB
TypeScript
/*
|
|
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 { M_POLL_START } from "matrix-events-sdk";
|
|
|
|
import { EventType, type IEvent, MatrixEvent, PollEvent, Room } 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 { isPollEvent, Poll } from "../../../src/models/poll";
|
|
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
|
import { flushPromises } from "../../test-utils/flushPromises";
|
|
import { mkEvent } from "../../test-utils/test-utils";
|
|
|
|
jest.useFakeTimers();
|
|
|
|
describe("Poll", () => {
|
|
const userId = "@alice:server.org";
|
|
const mockClient = getMockClientWithEventEmitter({
|
|
...mockClientMethodsUser(userId),
|
|
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
|
|
relations: jest.fn(),
|
|
});
|
|
const roomId = "!room:server";
|
|
const room = new Room(roomId, mockClient, userId);
|
|
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
|
|
// 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,
|
|
sender: userId,
|
|
});
|
|
basePollStartEvent.event.event_id = "$12345";
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
jest.setSystemTime(now);
|
|
|
|
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
|
|
|
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
|
});
|
|
|
|
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, room);
|
|
expect(poll.roomId).toEqual(roomId);
|
|
expect(poll.pollId).toEqual(basePollStartEvent.getId());
|
|
expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent);
|
|
expect(poll.isEnded).toBe(false);
|
|
expect(poll.endEventId).toBe(undefined);
|
|
});
|
|
|
|
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, room)).toThrow("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, room)).toThrow("Invalid poll start event.");
|
|
});
|
|
|
|
describe("fetching responses", () => {
|
|
it("calls relations api and emits", async () => {
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
const emitSpy = jest.spyOn(poll, "emit");
|
|
const fetchResponsePromise = poll.getResponses();
|
|
expect(poll.isFetchingResponses).toBe(true);
|
|
const responses = await fetchResponsePromise;
|
|
expect(poll.isFetchingResponses).toBe(false);
|
|
expect(mockClient.relations).toHaveBeenCalledWith(
|
|
roomId,
|
|
basePollStartEvent.getId(),
|
|
"m.reference",
|
|
undefined,
|
|
{ from: undefined },
|
|
);
|
|
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
|
});
|
|
|
|
it("returns existing responses object after initial fetch", async () => {
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
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, room);
|
|
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 = makeRelatedEvent({ 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, room);
|
|
const responses = await poll.getResponses();
|
|
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
|
|
});
|
|
|
|
describe("with multiple pages of relations", () => {
|
|
const makeResponses = (count = 1, timestamp = now): MatrixEvent[] =>
|
|
new Array(count)
|
|
.fill("x")
|
|
.map((_x, index) =>
|
|
makeRelatedEvent(
|
|
{ type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" },
|
|
timestamp + index,
|
|
),
|
|
);
|
|
|
|
it("page relations responses", async () => {
|
|
const responseEvents = makeResponses(6);
|
|
mockClient.relations
|
|
.mockResolvedValueOnce({
|
|
events: responseEvents.slice(0, 2),
|
|
nextBatch: "test-next-1",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
events: responseEvents.slice(2, 4),
|
|
nextBatch: "test-next-2",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
events: responseEvents.slice(4),
|
|
});
|
|
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
jest.spyOn(poll, "emit");
|
|
const responses = await poll.getResponses();
|
|
|
|
await flushPromises();
|
|
|
|
expect(mockClient.relations.mock.calls).toEqual([
|
|
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }],
|
|
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }],
|
|
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }],
|
|
]);
|
|
|
|
expect(poll.emit).toHaveBeenCalledTimes(3);
|
|
expect(poll.isFetchingResponses).toBeFalsy();
|
|
expect(responses.getRelations().length).toEqual(6);
|
|
});
|
|
});
|
|
|
|
describe("undecryptable relations", () => {
|
|
it("counts undecryptable relation events when getting responses", async () => {
|
|
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
|
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
|
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
|
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
|
|
|
mockClient.relations.mockResolvedValue({
|
|
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
|
});
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
jest.spyOn(poll, "emit");
|
|
await poll.getResponses();
|
|
expect(poll.undecryptableRelationsCount).toBe(1);
|
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
|
|
});
|
|
|
|
it("adds to undercryptable event count when new relation is undecryptable", async () => {
|
|
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
|
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
|
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
|
const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
|
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
|
jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
|
|
|
|
mockClient.relations.mockResolvedValue({
|
|
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
|
});
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
jest.spyOn(poll, "emit");
|
|
await poll.getResponses();
|
|
expect(poll.undecryptableRelationsCount).toBe(1);
|
|
|
|
await poll.onNewRelation(undecryptableEvent2);
|
|
|
|
expect(poll.undecryptableRelationsCount).toBe(2);
|
|
|
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 2);
|
|
});
|
|
});
|
|
|
|
describe("with poll end event", () => {
|
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
|
|
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
|
|
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, room);
|
|
jest.spyOn(poll, "emit");
|
|
await poll.getResponses();
|
|
|
|
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
|
expect(poll.isEnded).toBe(true);
|
|
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
|
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
|
});
|
|
|
|
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
|
|
const pollStartEvent = new MatrixEvent({
|
|
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
|
room_id: roomId,
|
|
sender: "@bob:domain.org",
|
|
});
|
|
pollStartEvent.event.event_id = "$6789";
|
|
const poll = new Poll(pollStartEvent, mockClient, room);
|
|
const pollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob:domain.org" });
|
|
mockClient.relations.mockResolvedValue({
|
|
events: [pollEndEvent],
|
|
});
|
|
maySendRedactionForEventSpy.mockReturnValue(false);
|
|
jest.spyOn(poll, "emit");
|
|
await poll.getResponses();
|
|
|
|
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
|
|
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, room);
|
|
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, room);
|
|
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, room);
|
|
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, room);
|
|
jest.spyOn(poll, "emit");
|
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
|
|
|
poll.onNewRelation(stablePollEndEvent);
|
|
|
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
|
});
|
|
|
|
it("does not set poll end event when sent by invalid user", async () => {
|
|
maySendRedactionForEventSpy.mockReturnValue(false);
|
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" });
|
|
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
|
mockClient.relations.mockResolvedValue({
|
|
events: [responseEventAfterEnd],
|
|
});
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
await poll.getResponses();
|
|
jest.spyOn(poll, "emit");
|
|
|
|
poll.onNewRelation(stablePollEndEvent);
|
|
|
|
// didn't end, didn't refilter responses
|
|
expect(poll.emit).not.toHaveBeenCalled();
|
|
expect(poll.isEnded).toBeFalsy();
|
|
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
|
|
});
|
|
|
|
it("replaces poll end event and refilters when an older end event already exists", async () => {
|
|
const earlierPollEndEvent = makeRelatedEvent(
|
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
|
now,
|
|
);
|
|
const laterPollEndEvent = makeRelatedEvent(
|
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
|
now + 2000,
|
|
);
|
|
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, laterPollEndEvent],
|
|
});
|
|
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
const responses = await poll.getResponses();
|
|
|
|
// all responses have a timestamp < laterPollEndEvent
|
|
expect(responses.getRelations().length).toEqual(3);
|
|
// first end event set correctly
|
|
expect(poll.isEnded).toBeTruthy();
|
|
|
|
// reset spy count
|
|
jest.spyOn(poll, "emit").mockClear();
|
|
|
|
// add a valid end event with earlier timestamp
|
|
poll.onNewRelation(earlierPollEndEvent);
|
|
|
|
// emitted new end event
|
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
|
// filtered responses and emitted
|
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
|
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
|
});
|
|
|
|
it("does not set poll end event when an earlier end event already exists", async () => {
|
|
const earlierPollEndEvent = makeRelatedEvent(
|
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
|
now,
|
|
);
|
|
const laterPollEndEvent = makeRelatedEvent(
|
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
|
now + 2000,
|
|
);
|
|
|
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
await poll.getResponses();
|
|
|
|
poll.onNewRelation(earlierPollEndEvent);
|
|
|
|
// first end event set correctly
|
|
expect(poll.isEnded).toBeTruthy();
|
|
|
|
// reset spy count
|
|
jest.spyOn(poll, "emit").mockClear();
|
|
|
|
poll.onNewRelation(laterPollEndEvent);
|
|
// didn't set new end event, didn't refilter responses
|
|
expect(poll.emit).not.toHaveBeenCalled();
|
|
expect(poll.isEnded).toBeTruthy();
|
|
});
|
|
|
|
it("sets poll end event and refilters responses based on timestamp", async () => {
|
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
|
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, room);
|
|
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, room);
|
|
// 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, room);
|
|
// 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]);
|
|
});
|
|
});
|
|
|
|
describe("isPollEvent", () => {
|
|
it("should return »false« for a non-poll event", () => {
|
|
const messageEvent = mkEvent({
|
|
event: true,
|
|
type: EventType.RoomMessage,
|
|
content: {},
|
|
user: mockClient.getSafeUserId(),
|
|
room: room.roomId,
|
|
});
|
|
expect(isPollEvent(messageEvent)).toBe(false);
|
|
});
|
|
|
|
it.each([[M_POLL_START.name], [M_POLL_RESPONSE.name], [M_POLL_END.name]])(
|
|
"should return »true« for a »%s« event",
|
|
(type: string) => {
|
|
const pollEvent = mkEvent({
|
|
event: true,
|
|
type,
|
|
content: {},
|
|
user: mockClient.getSafeUserId(),
|
|
room: room.roomId,
|
|
});
|
|
expect(isPollEvent(pollEvent)).toBe(true);
|
|
},
|
|
);
|
|
});
|
|
});
|