Live location sharing: only share to beacons created on device (#8378)

* create live beacons in ownbeaconstore and test

Signed-off-by: Kerry Archibald <kerrya@element.io>

* more mocks in RoomLiveShareWarning

Signed-off-by: Kerry Archibald <kerrya@element.io>

* extend mocks in components

Signed-off-by: Kerry Archibald <kerrya@element.io>

* comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove another comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* extra ? hedge in roommembers change

Signed-off-by: Kerry Archibald <kerrya@element.io>

* listen to destroy and prune local store on stop

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tests

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update copy pasted copyright to 2022

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-04-22 14:05:36 +02:00 committed by GitHub
parent a3a7c60dd7
commit 988d300258
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 341 additions and 20 deletions

View file

@ -23,7 +23,7 @@ import {
RoomStateEvent,
RoomMember,
} from "matrix-js-sdk/src/matrix";
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
import { makeBeaconContent, makeBeaconInfoContent } from "matrix-js-sdk/src/content-helpers";
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import { logger } from "matrix-js-sdk/src/logger";
@ -64,6 +64,7 @@ describe('OwnBeaconStore', () => {
getVisibleRooms: jest.fn().mockReturnValue([]),
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
sendEvent: jest.fn().mockResolvedValue({ event_id: '1' }),
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
});
const room1Id = '$room1:server.org';
const room2Id = '$room2:server.org';
@ -144,6 +145,7 @@ describe('OwnBeaconStore', () => {
beaconInfoEvent.getSender(),
beaconInfoEvent.getRoomId(),
{ isLive, timeout: beacon.beaconInfo.timeout },
'update-event-id',
);
beacon.update(updateEvent);
@ -156,6 +158,9 @@ describe('OwnBeaconStore', () => {
mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon);
};
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem').mockImplementation(() => {});
beforeEach(() => {
geolocation = mockGeolocation();
mockClient.getVisibleRooms.mockReturnValue([]);
@ -164,6 +169,9 @@ describe('OwnBeaconStore', () => {
jest.spyOn(global.Date, 'now').mockReturnValue(now);
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
jest.spyOn(logger, 'error').mockRestore();
localStorageGetSpy.mockClear().mockReturnValue(undefined);
localStorageSetSpy.mockClear();
});
afterEach(async () => {
@ -172,6 +180,10 @@ describe('OwnBeaconStore', () => {
jest.clearAllTimers();
});
afterAll(() => {
localStorageGetSpy.mockRestore();
});
describe('onReady()', () => {
it('initialises correctly with no beacons', async () => {
makeRoomsWithStateEvents();
@ -195,7 +207,27 @@ describe('OwnBeaconStore', () => {
bobsOldRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
expect(store.hasLiveBeacons()).toBe(true);
expect(store.beaconsByRoomId.get(room1Id)).toEqual(new Set([
getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
]));
expect(store.beaconsByRoomId.get(room2Id)).toEqual(new Set([
getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
]));
});
it('updates live beacon ids when users own beacons were created on device', async () => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo,
bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
expect(store.hasLiveBeacons(room1Id)).toBeTruthy();
expect(store.getLiveBeaconIds()).toEqual([
getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
@ -214,6 +246,10 @@ describe('OwnBeaconStore', () => {
});
it('does geolocation and sends location immediatley when user has live beacons', async () => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo,
@ -245,7 +281,8 @@ describe('OwnBeaconStore', () => {
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([BeaconEvent.Update]));
expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([BeaconEvent.Destroy]));
expect(removeSpy.mock.calls[4]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
});
it('destroys beacons', async () => {
@ -270,6 +307,10 @@ describe('OwnBeaconStore', () => {
bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo,
]);
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
});
it('returns true when user has live beacons', async () => {
@ -320,6 +361,10 @@ describe('OwnBeaconStore', () => {
bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo,
]);
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
});
it('returns live beacons when user has live beacons', async () => {
@ -371,6 +416,13 @@ describe('OwnBeaconStore', () => {
});
describe('on new beacon event', () => {
// assume all beacons were created on this device
beforeEach(() => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
});
it('ignores events for irrelevant beacons', async () => {
makeRoomsWithStateEvents([]);
const store = await makeOwnBeaconStore();
@ -425,6 +477,16 @@ describe('OwnBeaconStore', () => {
});
describe('on liveness change event', () => {
// assume all beacons were created on this device
beforeEach(() => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
alicesOldRoomIdBeaconInfo.getId(),
'update-event-id',
]));
});
it('ignores events for irrelevant beacons', async () => {
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
@ -501,6 +563,13 @@ describe('OwnBeaconStore', () => {
});
describe('on room membership changes', () => {
// assume all beacons were created on this device
beforeEach(() => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
});
it('ignores events for rooms without beacons', async () => {
const membershipEvent = makeMembershipEvent(room2Id, aliceId);
// no beacons for room2
@ -606,6 +675,54 @@ describe('OwnBeaconStore', () => {
});
});
describe('on destroy event', () => {
// assume all beacons were created on this device
beforeEach(() => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
alicesOldRoomIdBeaconInfo.getId(),
'update-event-id',
]));
});
it('ignores events for irrelevant beacons', async () => {
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
const emitSpy = jest.spyOn(store, 'emit');
const oldLiveBeaconIds = store.getLiveBeaconIds();
const bobsLiveBeacon = new Beacon(bobsRoom1BeaconInfo);
mockClient.emit(BeaconEvent.Destroy, bobsLiveBeacon.identifier);
expect(emitSpy).not.toHaveBeenCalled();
// strictly equal
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
});
it('updates state and emits beacon liveness changes from true to false', async () => {
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
// live before
expect(store.hasLiveBeacons()).toBe(true);
const emitSpy = jest.spyOn(store, 'emit');
const beacon = store.getBeaconById(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
beacon.destroy();
mockClient.emit(BeaconEvent.Destroy, beacon.identifier);
expect(store.hasLiveBeacons()).toBe(false);
expect(store.hasLiveBeacons(room1Id)).toBe(false);
expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, []);
});
});
describe('stopBeacon()', () => {
beforeEach(() => {
makeRoomsWithStateEvents([
@ -672,9 +789,38 @@ describe('OwnBeaconStore', () => {
expectedUpdateContent,
);
});
it('removes beacon event id from local store', async () => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
await store.stopBeacon(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
expect(localStorageSetSpy).toHaveBeenCalledWith(
'mx_live_beacon_created_id',
// stopped beacon's event_id was removed
JSON.stringify([alicesRoom2BeaconInfo.getId()]),
);
});
});
describe('publishing positions', () => {
// assume all beacons were created on this device
beforeEach(() => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
alicesOldRoomIdBeaconInfo.getId(),
'update-event-id',
]));
});
it('stops watching position when user has no more live beacons', async () => {
// geolocation is only going to emit 1 position
geolocation.watchPosition.mockImplementation(
@ -842,6 +988,7 @@ describe('OwnBeaconStore', () => {
// called for each position from watchPosition
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
expect(store.getLiveBeaconIdsWithWireError()).toEqual([]);
expect(store.hasWireErrors()).toBe(false);
});
@ -892,6 +1039,12 @@ describe('OwnBeaconStore', () => {
// only two allowed failures
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true);
expect(store.getLiveBeaconIdsWithWireError()).toEqual(
[getBeaconInfoIdentifier(alicesRoom1BeaconInfo)],
);
expect(store.getLiveBeaconIdsWithWireError(room1Id)).toEqual(
[getBeaconInfoIdentifier(alicesRoom1BeaconInfo)],
);
expect(store.hasWireErrors()).toBe(true);
expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
@ -1055,4 +1208,79 @@ describe('OwnBeaconStore', () => {
expect(mockClient.sendEvent).not.toHaveBeenCalled();
});
});
describe('createLiveBeacon', () => {
const newEventId = 'new-beacon-event-id';
const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
beforeEach(() => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
]));
localStorageSetSpy.mockClear();
mockClient.unstable_createLiveBeacon.mockResolvedValue({ event_id: newEventId });
});
it('creates a live beacon', async () => {
const store = await makeOwnBeaconStore();
const content = makeBeaconInfoContent(100);
await store.createLiveBeacon(room1Id, content);
expect(mockClient.unstable_createLiveBeacon).toHaveBeenCalledWith(room1Id, content);
});
it('sets new beacon event id in local storage', async () => {
const store = await makeOwnBeaconStore();
const content = makeBeaconInfoContent(100);
await store.createLiveBeacon(room1Id, content);
expect(localStorageSetSpy).toHaveBeenCalledWith(
'mx_live_beacon_created_id',
JSON.stringify([
alicesRoom1BeaconInfo.getId(),
newEventId,
]),
);
});
it('handles saving beacon event id when local storage has bad value', async () => {
localStorageGetSpy.mockReturnValue(JSON.stringify({ id: '1' }));
const store = await makeOwnBeaconStore();
const content = makeBeaconInfoContent(100);
await store.createLiveBeacon(room1Id, content);
// stored successfully
expect(localStorageSetSpy).toHaveBeenCalledWith(
'mx_live_beacon_created_id',
JSON.stringify([
newEventId,
]),
);
});
it('creates a live beacon without error when no beacons exist for room', async () => {
const store = await makeOwnBeaconStore();
const content = makeBeaconInfoContent(100);
await store.createLiveBeacon(room1Id, content);
// didn't throw, no error log
expect(loggerErrorSpy).not.toHaveBeenCalled();
});
it('stops live beacons for room after creating new beacon', async () => {
// room1 already has a beacon
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
// but it was not created on this device
localStorageGetSpy.mockReturnValue(undefined);
const store = await makeOwnBeaconStore();
const content = makeBeaconInfoContent(100);
await store.createLiveBeacon(room1Id, content);
// update beacon called
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
});
});
});