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:
parent
a3a7c60dd7
commit
988d300258
7 changed files with 341 additions and 20 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue