Skip to content

Commit 9b843da

Browse files
author
Kerry
authored
Live location share - add start time leniency (PSF-1081) (#2465)
* remove some of the confusing time travel in beacon.spec * test cases * add start time leniency to beacon liveness check
1 parent ab588f0 commit 9b843da

File tree

3 files changed

+154
-23
lines changed

3 files changed

+154
-23
lines changed

spec/test-utils/beacon.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type InfoContentProps = {
2727
isLive?: boolean;
2828
assetType?: LocationAssetType;
2929
description?: string;
30+
timestamp?: number;
3031
};
3132
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
3233
timeout: 3600000,
@@ -44,7 +45,11 @@ export const makeBeaconInfoEvent = (
4445
eventId?: string,
4546
): MatrixEvent => {
4647
const {
47-
timeout, isLive, description, assetType,
48+
timeout,
49+
isLive,
50+
description,
51+
assetType,
52+
timestamp,
4853
} = {
4954
...DEFAULT_INFO_CONTENT_PROPS,
5055
...contentProps,
@@ -53,10 +58,10 @@ export const makeBeaconInfoEvent = (
5358
type: M_BEACON_INFO.name,
5459
room_id: roomId,
5560
state_key: sender,
56-
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
61+
content: makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),
5762
});
5863

59-
event.event.origin_server_ts = Date.now();
64+
event.event.origin_server_ts = timestamp || Date.now();
6065

6166
// live beacons use the beacon_info event id
6267
// set or default this

spec/unit/models/beacon.spec.ts

+137-19
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { MatrixEvent } from "../../../src";
1718
import {
1819
isTimestampInDuration,
1920
Beacon,
@@ -65,9 +66,9 @@ describe('Beacon', () => {
6566
// beacon_info events
6667
// created 'an hour ago'
6768
// without timeout of 3 hours
68-
let liveBeaconEvent;
69-
let notLiveBeaconEvent;
70-
let user2BeaconEvent;
69+
let liveBeaconEvent: MatrixEvent;
70+
let notLiveBeaconEvent: MatrixEvent;
71+
let user2BeaconEvent: MatrixEvent;
7172

7273
const advanceDateAndTime = (ms: number) => {
7374
// bc liveness check uses Date.now we have to advance this mock
@@ -77,21 +78,24 @@ describe('Beacon', () => {
7778
};
7879

7980
beforeEach(() => {
80-
// go back in time to create the beacon
81-
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS);
8281
liveBeaconEvent = makeBeaconInfoEvent(
8382
userId,
8483
roomId,
8584
{
8685
timeout: HOUR_MS * 3,
8786
isLive: true,
87+
timestamp: now - HOUR_MS,
8888
},
8989
'$live123',
9090
);
9191
notLiveBeaconEvent = makeBeaconInfoEvent(
9292
userId,
9393
roomId,
94-
{ timeout: HOUR_MS * 3, isLive: false },
94+
{
95+
timeout: HOUR_MS * 3,
96+
isLive: false,
97+
timestamp: now - HOUR_MS,
98+
},
9599
'$dead123',
96100
);
97101
user2BeaconEvent = makeBeaconInfoEvent(
@@ -100,11 +104,12 @@ describe('Beacon', () => {
100104
{
101105
timeout: HOUR_MS * 3,
102106
isLive: true,
107+
timestamp: now - HOUR_MS,
103108
},
104109
'$user2live123',
105110
);
106111

107-
// back to now
112+
// back to 'now'
108113
jest.spyOn(global.Date, 'now').mockReturnValue(now);
109114
});
110115

@@ -131,17 +136,81 @@ describe('Beacon', () => {
131136
});
132137

133138
it('returns false when beacon is expired', () => {
134-
// time travel to beacon creation + 3 hours
135-
jest.spyOn(global.Date, 'now').mockReturnValue(now - 3 * HOUR_MS);
136-
const beacon = new Beacon(liveBeaconEvent);
139+
const expiredBeaconEvent = makeBeaconInfoEvent(
140+
userId2,
141+
roomId,
142+
{
143+
timeout: HOUR_MS,
144+
isLive: true,
145+
timestamp: now - HOUR_MS * 2,
146+
},
147+
'$user2live123',
148+
);
149+
const beacon = new Beacon(expiredBeaconEvent);
137150
expect(beacon.isLive).toEqual(false);
138151
});
139152

140-
it('returns false when beacon timestamp is in future', () => {
141-
// time travel to before beacon events timestamp
142-
// event was created now - 1 hour
143-
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS - HOUR_MS);
144-
const beacon = new Beacon(liveBeaconEvent);
153+
it('returns false when beacon timestamp is in future by an hour', () => {
154+
const beaconStartsInHour = makeBeaconInfoEvent(
155+
userId2,
156+
roomId,
157+
{
158+
timeout: HOUR_MS,
159+
isLive: true,
160+
timestamp: now + HOUR_MS,
161+
},
162+
'$user2live123',
163+
);
164+
const beacon = new Beacon(beaconStartsInHour);
165+
expect(beacon.isLive).toEqual(false);
166+
});
167+
168+
it('returns true when beacon timestamp is one minute in the future', () => {
169+
const beaconStartsInOneMin = makeBeaconInfoEvent(
170+
userId2,
171+
roomId,
172+
{
173+
timeout: HOUR_MS,
174+
isLive: true,
175+
timestamp: now + 60000,
176+
},
177+
'$user2live123',
178+
);
179+
const beacon = new Beacon(beaconStartsInOneMin);
180+
expect(beacon.isLive).toEqual(true);
181+
});
182+
183+
it('returns true when beacon timestamp is one minute before expiry', () => {
184+
// this test case is to check the start time leniency doesn't affect
185+
// strict expiry time checks
186+
const expiresInOneMin = makeBeaconInfoEvent(
187+
userId2,
188+
roomId,
189+
{
190+
timeout: HOUR_MS,
191+
isLive: true,
192+
timestamp: now - HOUR_MS + 60000,
193+
},
194+
'$user2live123',
195+
);
196+
const beacon = new Beacon(expiresInOneMin);
197+
expect(beacon.isLive).toEqual(true);
198+
});
199+
200+
it('returns false when beacon timestamp is one minute after expiry', () => {
201+
// this test case is to check the start time leniency doesn't affect
202+
// strict expiry time checks
203+
const expiredOneMinAgo = makeBeaconInfoEvent(
204+
userId2,
205+
roomId,
206+
{
207+
timeout: HOUR_MS,
208+
isLive: true,
209+
timestamp: now - HOUR_MS - 60000,
210+
},
211+
'$user2live123',
212+
);
213+
const beacon = new Beacon(expiredOneMinAgo);
145214
expect(beacon.isLive).toEqual(false);
146215
});
147216

@@ -232,19 +301,17 @@ describe('Beacon', () => {
232301
});
233302

234303
it('checks liveness of beacon at expected start time', () => {
235-
// go forward in time to make beacon with timestamp in future
236-
jest.spyOn(global.Date, 'now').mockReturnValue(now + HOUR_MS);
237304
const futureBeaconEvent = makeBeaconInfoEvent(
238305
userId,
239306
roomId,
240307
{
241308
timeout: HOUR_MS * 3,
242309
isLive: true,
310+
// start timestamp hour in future
311+
timestamp: now + HOUR_MS,
243312
},
244313
'$live123',
245314
);
246-
// go back to now
247-
jest.spyOn(global.Date, 'now').mockReturnValue(now);
248315

249316
const beacon = new Beacon(futureBeaconEvent);
250317
expect(beacon.isLive).toBeFalsy();
@@ -345,6 +412,57 @@ describe('Beacon', () => {
345412
expect(emitSpy).not.toHaveBeenCalled();
346413
});
347414

415+
describe('when beacon is live with a start timestamp is in the future', () => {
416+
it('ignores locations before the beacon start timestamp', () => {
417+
const startTimestamp = now + 60000;
418+
const beacon = new Beacon(makeBeaconInfoEvent(
419+
userId,
420+
roomId,
421+
{ isLive: true, timeout: 60000, timestamp: startTimestamp },
422+
));
423+
const emitSpy = jest.spyOn(beacon, 'emit');
424+
425+
beacon.addLocations([
426+
// beacon has now + 60000 live period
427+
makeBeaconEvent(
428+
userId,
429+
{
430+
beaconInfoId: beacon.beaconInfoId,
431+
// now < location timestamp < beacon timestamp
432+
timestamp: now + 10,
433+
},
434+
),
435+
]);
436+
437+
expect(beacon.latestLocationState).toBeFalsy();
438+
expect(emitSpy).not.toHaveBeenCalled();
439+
});
440+
it('sets latest location when location timestamp is after startTimestamp', () => {
441+
const startTimestamp = now + 60000;
442+
const beacon = new Beacon(makeBeaconInfoEvent(
443+
userId,
444+
roomId,
445+
{ isLive: true, timeout: 600000, timestamp: startTimestamp },
446+
));
447+
const emitSpy = jest.spyOn(beacon, 'emit');
448+
449+
beacon.addLocations([
450+
// beacon has now + 600000 live period
451+
makeBeaconEvent(
452+
userId,
453+
{
454+
beaconInfoId: beacon.beaconInfoId,
455+
// now < beacon timestamp < location timestamp
456+
timestamp: startTimestamp + 10,
457+
},
458+
),
459+
]);
460+
461+
expect(beacon.latestLocationState).toBeTruthy();
462+
expect(emitSpy).toHaveBeenCalled();
463+
});
464+
});
465+
348466
it('sets latest location state to most recent location', () => {
349467
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
350468
const emitSpy = jest.spyOn(beacon, 'emit');

src/models/beacon.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,16 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
185185

186186
private checkLiveness(): void {
187187
const prevLiveness = this.isLive;
188+
189+
// element web sets a beacon's start timestamp to the senders local current time
190+
// when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live
191+
// may have a start timestamp in the future from Bob's POV
192+
// handle this by adding 6min of leniency to the start timestamp when it is in the future
193+
const startTimestamp = this._beaconInfo?.timestamp > Date.now() ?
194+
this._beaconInfo?.timestamp - 360000 /* 6min */ :
195+
this._beaconInfo?.timestamp;
188196
this._isLive = this._beaconInfo?.live &&
189-
isTimestampInDuration(this._beaconInfo?.timestamp, this._beaconInfo?.timeout, Date.now());
197+
isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now());
190198

191199
if (prevLiveness !== this.isLive) {
192200
this.emit(BeaconEvent.LivenessChange, this.isLive, this);

0 commit comments

Comments
 (0)