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 12:05:36 +00:00
committed by GitHub
parent a3a7c60dd7
commit 988d300258
7 changed files with 341 additions and 20 deletions
@@ -93,10 +93,20 @@ describe('<RoomLiveShareWarning />', () => {
return component;
};
const localStorageSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
beforeEach(() => {
mockGeolocation();
jest.spyOn(global.Date, 'now').mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });
// assume all beacons were created on this device
localStorageSpy.mockReturnValue(JSON.stringify([
room1Beacon1.getId(),
room2Beacon1.getId(),
room2Beacon2.getId(),
room3Beacon1.getId(),
]));
});
afterEach(async () => {
@@ -106,6 +116,7 @@ describe('<RoomLiveShareWarning />', () => {
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
localStorageSpy.mockRestore();
});
const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();
@@ -29,9 +29,15 @@ import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
import SettingsStore from '../../../../src/settings/SettingsStore';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import { LocationShareType } from '../../../../src/components/views/location/shareLocation';
import { findByTagAndTestId, flushPromises } from '../../../test-utils';
import {
findByTagAndTestId,
flushPromises,
getMockClientWithEventEmitter,
setupAsyncStoreWithClient,
} from '../../../test-utils';
import Modal from '../../../../src/Modal';
import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown';
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({
findMapStyleUrl: jest.fn().mockReturnValue('test'),
@@ -57,17 +63,15 @@ jest.mock('../../../../src/Modal', () => ({
describe('<LocationShareMenu />', () => {
const userId = '@ernie:server.org';
const mockClient = {
on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(),
const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(userId),
getClientWellKnown: jest.fn().mockResolvedValue({
map_style_url: 'maps.com',
}),
sendMessage: jest.fn(),
unstable_createLiveBeacon: jest.fn().mockResolvedValue({}),
};
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
getVisibleRooms: jest.fn().mockReturnValue([]),
});
const defaultProps = {
menuPosition: {
@@ -90,19 +94,28 @@ describe('<LocationShareMenu />', () => {
type: 'geolocate',
};
const makeOwnBeaconStore = async () => {
const store = OwnBeaconStore.instance;
await setupAsyncStoreWithClient(store, mockClient);
return store;
};
const getComponent = (props = {}) =>
mount(<LocationShareMenu {...defaultProps} {...props} />, {
wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: { value: mockClient },
});
beforeEach(() => {
beforeEach(async () => {
jest.spyOn(logger, 'error').mockRestore();
mocked(SettingsStore).getValue.mockReturnValue(false);
mockClient.sendMessage.mockClear();
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue(undefined);
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
mocked(Modal).createTrackedDialog.mockClear();
await makeOwnBeaconStore();
});
const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) =>
+231 -3
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();
});
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.