1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-06-08 15:21:53 +03:00
matrix-js-sdk/spec/unit/models/beacon.spec.ts
Will Hunt f6a169b5a5
Replace usages of global with globalThis (#4489)
* Update src with globalThis

* Update spec with globalThis

* Replace in more spec/ places

* More changes to src/

* Add a linter rule for global

* Prettify

* lint
2024-11-01 09:15:21 +00:00

573 lines
22 KiB
TypeScript

/*
Copyright 2022 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 { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers();
describe("Beacon", () => {
describe("isTimestampInDuration()", () => {
const startTs = new Date("2022-03-11T12:07:47.592Z").getTime();
const HOUR_MS = 3600000;
it("returns false when timestamp is before start time", () => {
// day before
const timestamp = new Date("2022-03-10T12:07:47.592Z").getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
});
it("returns false when timestamp is after start time + duration", () => {
// 1 second later
const timestamp = new Date("2022-03-10T12:07:48.592Z").getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
});
it("returns true when timestamp is exactly start time", () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true);
});
it("returns true when timestamp is exactly the end of the duration", () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true);
});
it("returns true when timestamp is within the duration", () => {
const twoHourDuration = HOUR_MS * 2;
const now = startTs + HOUR_MS;
expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true);
});
});
describe("Beacon", () => {
const userId = "@user:server.org";
const userId2 = "@user2:server.org";
const roomId = "$room:server.org";
// 14.03.2022 16:15
const now = 1647270879403;
const HOUR_MS = 3600000;
// beacon_info events
// created 'an hour ago'
// without timeout of 3 hours
let liveBeaconEvent: MatrixEvent;
let notLiveBeaconEvent: MatrixEvent;
let user2BeaconEvent: MatrixEvent;
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
beforeEach(() => {
liveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
timestamp: now - HOUR_MS,
},
"$live123",
);
notLiveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: false,
timestamp: now - HOUR_MS,
},
"$dead123",
);
user2BeaconEvent = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
timestamp: now - HOUR_MS,
},
"$user2live123",
);
// back to 'now'
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(globalThis.Date, "now").mockRestore();
});
it("creates beacon from event", () => {
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(true);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
it("creates beacon without error from a malformed event", () => {
const event = new MatrixEvent({
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: userId,
content: {},
});
const beacon = new Beacon(event);
expect(beacon.beaconInfoId).toEqual(event.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(false);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
describe("isLive()", () => {
it("returns false when beacon is explicitly set to not live", () => {
const beacon = new Beacon(notLiveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it("returns false when beacon is expired", () => {
const expiredBeaconEvent = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now - HOUR_MS * 2,
},
"$user2live123",
);
const beacon = new Beacon(expiredBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it("returns false when beacon timestamp is in future by an hour", () => {
const beaconStartsInHour = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now + HOUR_MS,
},
"$user2live123",
);
const beacon = new Beacon(beaconStartsInHour);
expect(beacon.isLive).toEqual(false);
});
it("returns true when beacon timestamp is one minute in the future", () => {
const beaconStartsInOneMin = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now + 60000,
},
"$user2live123",
);
const beacon = new Beacon(beaconStartsInOneMin);
expect(beacon.isLive).toEqual(true);
});
it("returns true when beacon timestamp is one minute before expiry", () => {
// this test case is to check the start time leniency doesn't affect
// strict expiry time checks
const expiresInOneMin = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now - HOUR_MS + 60000,
},
"$user2live123",
);
const beacon = new Beacon(expiresInOneMin);
expect(beacon.isLive).toEqual(true);
});
it("returns false when beacon timestamp is one minute after expiry", () => {
// this test case is to check the start time leniency doesn't affect
// strict expiry time checks
const expiredOneMinAgo = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now - HOUR_MS - 60000,
},
"$user2live123",
);
const beacon = new Beacon(expiredOneMinAgo);
expect(beacon.isLive).toEqual(false);
});
it("returns true when beacon was created in past and not yet expired", () => {
// liveBeaconEvent was created 1 hour ago
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(true);
});
});
describe("update()", () => {
it("does not update with different event", () => {
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(() => beacon.update(user2BeaconEvent)).toThrow();
// didnt update
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
});
it("does not update with an older event", () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit").mockClear();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
const oldUpdateEvent = makeBeaconInfoEvent(userId, roomId);
// less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000;
beacon.update(oldUpdateEvent);
// didnt update
expect(emitSpy).not.toHaveBeenCalled();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
});
it("updates event", () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit");
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
"$live123",
);
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon);
});
it("emits livenesschange event when beacon liveness changes", () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit");
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
beacon.beaconInfoId,
);
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
});
describe("monitorLiveness()", () => {
it("does not set a monitor interval when beacon is not live", () => {
// beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(notLiveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchTimeout).toBeFalsy();
advanceDateAndTime(HOUR_MS * 2 + 1);
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
it("checks liveness of beacon at expected start time", () => {
const futureBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
// start timestamp hour in future
timestamp: now + HOUR_MS,
},
"$live123",
);
const beacon = new Beacon(futureBeaconEvent);
expect(beacon.isLive).toBeFalsy();
const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness();
// advance to the start timestamp of the beacon
advanceDateAndTime(HOUR_MS + 1);
// beacon is in live period now
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, true, beacon);
// check the expiry monitor is still setup ok
// advance to the expiry
advanceDateAndTime(HOUR_MS * 3 + 100);
expect(emitSpy).toHaveBeenCalledTimes(2);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
it("checks liveness of beacon at expected expiry time", () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness();
advanceDateAndTime(HOUR_MS * 2 + 1);
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
it("clears monitor interval when re-monitoring liveness", () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
beacon.monitorLiveness();
// @ts-ignore
const oldMonitor = beacon.livenessWatchTimeout;
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchTimeout).not.toEqual(oldMonitor);
});
it("destroy kills liveness monitor and emits", () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness();
// destroy the beacon
beacon.destroy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
// live forced to false
expect(beacon.isLive).toBe(false);
advanceDateAndTime(HOUR_MS * 2 + 1);
// no additional calls
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
describe("addLocations", () => {
it("ignores locations when beacon is not live", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it("ignores locations outside the beacon live duration", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([
// beacon has now + 60000 live period
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 100000 }),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, "emit");
const ev = new MatrixEvent({
type: M_BEACON_INFO.name,
sender: userId,
room_id: roomId,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beacon.beaconInfoId,
},
},
});
beacon.addLocations([ev]);
expect(beacon.latestLocationEvent).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
describe("when beacon is live with a start timestamp is in the future", () => {
it("ignores locations before the beacon start timestamp", () => {
const startTimestamp = now + 60000;
const beacon = new Beacon(
makeBeaconInfoEvent(userId, roomId, {
isLive: true,
timeout: 60000,
timestamp: startTimestamp,
}),
);
const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([
// beacon has now + 60000 live period
makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
// now < location timestamp < beacon timestamp
timestamp: now + 10,
}),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it("sets latest location when location timestamp is after startTimestamp", () => {
const startTimestamp = now + 60000;
const beacon = new Beacon(
makeBeaconInfoEvent(userId, roomId, {
isLive: true,
timeout: 600000,
timestamp: startTimestamp,
}),
);
const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([
// beacon has now + 600000 live period
makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
// now < beacon timestamp < location timestamp
timestamp: startTimestamp + 10,
}),
]);
expect(beacon.latestLocationState).toBeTruthy();
expect(emitSpy).toHaveBeenCalled();
});
});
it("sets latest location state to most recent location", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, "emit");
const locations = [
// older
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, uri: "geo:foo", timestamp: now + 1 }),
// newer
makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
uri: "geo:bar",
timestamp: now + 10000,
}),
// not valid
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, uri: "geo:baz", timestamp: now - 5 }),
];
beacon.addLocations(locations);
const expectedLatestLocation = {
description: undefined,
timestamp: now + 10000,
uri: "geo:bar",
};
// the newest valid location
expect(beacon.latestLocationState).toEqual(expectedLatestLocation);
expect(beacon.latestLocationEvent).toEqual(locations[1]);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
});
it("ignores locations that are less recent that the current latest location", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const olderLocation = makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
uri: "geo:foo",
timestamp: now + 1,
});
const newerLocation = makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
uri: "geo:bar",
timestamp: now + 10000,
});
beacon.addLocations([newerLocation]);
// latest location set to newerLocation
expect(beacon.latestLocationState).toEqual(
expect.objectContaining({
uri: "geo:bar",
}),
);
expect(beacon.latestLocationEvent).toEqual(newerLocation);
const emitSpy = jest.spyOn(beacon, "emit").mockClear();
// add older location
beacon.addLocations([olderLocation]);
// no change
expect(beacon.latestLocationState).toEqual(
expect.objectContaining({
uri: "geo:bar",
}),
);
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
});
});
});