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

@ -25,6 +25,7 @@ import { _t } from "../../../languageHandler";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog"; import QuestionDialog from "../dialogs/QuestionDialog";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { OwnBeaconStore } from "../../../stores/OwnBeaconStore";
export enum LocationShareType { export enum LocationShareType {
Own = 'Own', Own = 'Own',
@ -70,7 +71,7 @@ export const shareLiveLocation = (
): ShareLocationFn => async ({ timeout }) => { ): ShareLocationFn => async ({ timeout }) => {
const description = _t(`%(displayName)s's live location`, { displayName }); const description = _t(`%(displayName)s's live location`, { displayName });
try { try {
await client.unstable_createLiveBeacon( await OwnBeaconStore.instance.createLiveBeacon(
roomId, roomId,
makeBeaconInfoContent( makeBeaconInfoContent(
timeout ?? DEFAULT_LIVE_DURATION, timeout ?? DEFAULT_LIVE_DURATION,

View file

@ -28,7 +28,7 @@ import {
import { import {
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent, BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
} from "matrix-js-sdk/src/content-helpers"; } from "matrix-js-sdk/src/content-helpers";
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import { MBeaconInfoEventContent, M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import defaultDispatcher from "../dispatcher/dispatcher"; import defaultDispatcher from "../dispatcher/dispatcher";
@ -64,6 +64,30 @@ type OwnBeaconStoreState = {
beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>; beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
liveBeaconIds: BeaconIdentifier[]; liveBeaconIds: BeaconIdentifier[];
}; };
const CREATED_BEACONS_KEY = 'mx_live_beacon_created_id';
const removeLocallyCreateBeaconEventId = (eventId: string): void => {
const ids = getLocallyCreatedBeaconEventIds();
window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify(ids.filter(id => id !== eventId)));
};
const storeLocallyCreateBeaconEventId = (eventId: string): void => {
const ids = getLocallyCreatedBeaconEventIds();
window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify([...ids, eventId]));
};
const getLocallyCreatedBeaconEventIds = (): string[] => {
let ids: string[];
try {
ids = JSON.parse(window.localStorage.getItem(CREATED_BEACONS_KEY) ?? '[]');
if (!Array.isArray(ids)) {
throw new Error('Invalid stored value');
}
} catch (error) {
logger.error('Failed to retrieve locally created beacon event ids', error);
ids = [];
}
return ids;
};
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> { export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
private static internalInstance = new OwnBeaconStore(); private static internalInstance = new OwnBeaconStore();
// users beacons, keyed by event type // users beacons, keyed by event type
@ -110,6 +134,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness); this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon); this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon); this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon);
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers); this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
this.beacons.forEach(beacon => beacon.destroy()); this.beacons.forEach(beacon => beacon.destroy());
@ -125,6 +150,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness); this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon); this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon); this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon);
this.matrixClient.on(BeaconEvent.Destroy, this.onDestroyBeacon);
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers); this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
this.initialiseBeaconState(); this.initialiseBeaconState();
@ -188,7 +214,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return; return;
} }
return await this.updateBeaconEvent(beacon, { live: false }); await this.updateBeaconEvent(beacon, { live: false });
// prune from local store
removeLocallyCreateBeaconEventId(beacon.beaconInfoId);
}; };
/** /**
@ -215,6 +244,15 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
beacon.monitorLiveness(); beacon.monitorLiveness();
}; };
private onDestroyBeacon = (beaconIdentifier: BeaconIdentifier): void => {
// check if we care about this beacon
if (!this.beacons.has(beaconIdentifier)) {
return;
}
this.checkLiveness();
};
private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => { private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => {
// check if we care about this beacon // check if we care about this beacon
if (!this.beacons.has(beacon.identifier)) { if (!this.beacons.has(beacon.identifier)) {
@ -249,7 +287,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
// stop watching beacons in rooms where user is no longer a member // stop watching beacons in rooms where user is no longer a member
if (member.membership === 'leave' || member.membership === 'ban') { if (member.membership === 'leave' || member.membership === 'ban') {
this.beaconsByRoomId.get(roomState.roomId).forEach(this.removeBeacon); this.beaconsByRoomId.get(roomState.roomId)?.forEach(this.removeBeacon);
this.beaconsByRoomId.delete(roomState.roomId); this.beaconsByRoomId.delete(roomState.roomId);
} }
}; };
@ -308,9 +346,14 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
}; };
private checkLiveness = (): void => { private checkLiveness = (): void => {
const locallyCreatedBeaconEventIds = getLocallyCreatedBeaconEventIds();
const prevLiveBeaconIds = this.getLiveBeaconIds(); const prevLiveBeaconIds = this.getLiveBeaconIds();
this.liveBeaconIds = [...this.beacons.values()] this.liveBeaconIds = [...this.beacons.values()]
.filter(beacon => beacon.isLive) .filter(beacon =>
beacon.isLive &&
// only beacons created on this device should be shared to
locallyCreatedBeaconEventIds.includes(beacon.beaconInfoId),
)
.sort(sortBeaconsByLatestCreation) .sort(sortBeaconsByLatestCreation)
.map(beacon => beacon.identifier); .map(beacon => beacon.identifier);
@ -339,6 +382,32 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
} }
}; };
public createLiveBeacon = async (
roomId: Room['roomId'],
beaconInfoContent: MBeaconInfoEventContent,
): Promise<void> => {
// eslint-disable-next-line camelcase
const { event_id } = await this.matrixClient.unstable_createLiveBeacon(
roomId,
beaconInfoContent,
);
storeLocallyCreateBeaconEventId(event_id);
// try to stop any other live beacons
// in this room
this.beaconsByRoomId.get(roomId)?.forEach(beaconId => {
if (this.getBeaconById(beaconId)?.isLive) {
try {
// don't await, this is best effort
this.stopBeacon(beaconId);
} catch (error) {
logger.error('Failed to stop live beacons', error);
}
}
});
};
/** /**
* Geolocation * Geolocation
*/ */
@ -420,7 +489,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.stopPollingLocation(); this.stopPollingLocation();
// kill live beacons when location permissions are revoked // kill live beacons when location permissions are revoked
// TODO may need adjustment when PSF-797 is done
await Promise.all(this.liveBeaconIds.map(this.stopBeacon)); await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
}; };

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -93,10 +93,20 @@ describe('<RoomLiveShareWarning />', () => {
return component; return component;
}; };
const localStorageSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
beforeEach(() => { beforeEach(() => {
mockGeolocation(); mockGeolocation();
jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.spyOn(global.Date, 'now').mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' }); 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 () => { afterEach(async () => {
@ -106,6 +116,7 @@ describe('<RoomLiveShareWarning />', () => {
afterAll(() => { afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore(); jest.spyOn(global.Date, 'now').mockRestore();
localStorageSpy.mockRestore();
}); });
const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text(); const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();

View file

@ -29,9 +29,15 @@ import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
import SettingsStore from '../../../../src/settings/SettingsStore'; import SettingsStore from '../../../../src/settings/SettingsStore';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import { LocationShareType } from '../../../../src/components/views/location/shareLocation'; 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 Modal from '../../../../src/Modal';
import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown'; import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown';
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({
findMapStyleUrl: jest.fn().mockReturnValue('test'), findMapStyleUrl: jest.fn().mockReturnValue('test'),
@ -57,17 +63,15 @@ jest.mock('../../../../src/Modal', () => ({
describe('<LocationShareMenu />', () => { describe('<LocationShareMenu />', () => {
const userId = '@ernie:server.org'; const userId = '@ernie:server.org';
const mockClient = { const mockClient = getMockClientWithEventEmitter({
on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(),
getUserId: jest.fn().mockReturnValue(userId), getUserId: jest.fn().mockReturnValue(userId),
getClientWellKnown: jest.fn().mockResolvedValue({ getClientWellKnown: jest.fn().mockResolvedValue({
map_style_url: 'maps.com', map_style_url: 'maps.com',
}), }),
sendMessage: jest.fn(), sendMessage: jest.fn(),
unstable_createLiveBeacon: jest.fn().mockResolvedValue({}), unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
}; getVisibleRooms: jest.fn().mockReturnValue([]),
});
const defaultProps = { const defaultProps = {
menuPosition: { menuPosition: {
@ -90,19 +94,28 @@ describe('<LocationShareMenu />', () => {
type: 'geolocate', type: 'geolocate',
}; };
const makeOwnBeaconStore = async () => {
const store = OwnBeaconStore.instance;
await setupAsyncStoreWithClient(store, mockClient);
return store;
};
const getComponent = (props = {}) => const getComponent = (props = {}) =>
mount(<LocationShareMenu {...defaultProps} {...props} />, { mount(<LocationShareMenu {...defaultProps} {...props} />, {
wrappingComponent: MatrixClientContext.Provider, wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: { value: mockClient }, wrappingComponentProps: { value: mockClient },
}); });
beforeEach(() => { beforeEach(async () => {
jest.spyOn(logger, 'error').mockRestore(); jest.spyOn(logger, 'error').mockRestore();
mocked(SettingsStore).getValue.mockReturnValue(false); mocked(SettingsStore).getValue.mockReturnValue(false);
mockClient.sendMessage.mockClear(); 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); jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
mocked(Modal).createTrackedDialog.mockClear(); mocked(Modal).createTrackedDialog.mockClear();
await makeOwnBeaconStore();
}); });
const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) => const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) =>

View file

@ -23,7 +23,7 @@ import {
RoomStateEvent, RoomStateEvent,
RoomMember, RoomMember,
} from "matrix-js-sdk/src/matrix"; } 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 { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -64,6 +64,7 @@ describe('OwnBeaconStore', () => {
getVisibleRooms: jest.fn().mockReturnValue([]), getVisibleRooms: jest.fn().mockReturnValue([]),
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
sendEvent: 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 room1Id = '$room1:server.org';
const room2Id = '$room2:server.org'; const room2Id = '$room2:server.org';
@ -144,6 +145,7 @@ describe('OwnBeaconStore', () => {
beaconInfoEvent.getSender(), beaconInfoEvent.getSender(),
beaconInfoEvent.getRoomId(), beaconInfoEvent.getRoomId(),
{ isLive, timeout: beacon.beaconInfo.timeout }, { isLive, timeout: beacon.beaconInfo.timeout },
'update-event-id',
); );
beacon.update(updateEvent); beacon.update(updateEvent);
@ -156,6 +158,9 @@ describe('OwnBeaconStore', () => {
mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon); mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon);
}; };
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem').mockImplementation(() => {});
beforeEach(() => { beforeEach(() => {
geolocation = mockGeolocation(); geolocation = mockGeolocation();
mockClient.getVisibleRooms.mockReturnValue([]); mockClient.getVisibleRooms.mockReturnValue([]);
@ -164,6 +169,9 @@ describe('OwnBeaconStore', () => {
jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.spyOn(global.Date, 'now').mockReturnValue(now);
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore(); jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
jest.spyOn(logger, 'error').mockRestore(); jest.spyOn(logger, 'error').mockRestore();
localStorageGetSpy.mockClear().mockReturnValue(undefined);
localStorageSetSpy.mockClear();
}); });
afterEach(async () => { afterEach(async () => {
@ -172,6 +180,10 @@ describe('OwnBeaconStore', () => {
jest.clearAllTimers(); jest.clearAllTimers();
}); });
afterAll(() => {
localStorageGetSpy.mockRestore();
});
describe('onReady()', () => { describe('onReady()', () => {
it('initialises correctly with no beacons', async () => { it('initialises correctly with no beacons', async () => {
makeRoomsWithStateEvents(); makeRoomsWithStateEvents();
@ -195,7 +207,27 @@ describe('OwnBeaconStore', () => {
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
const store = await makeOwnBeaconStore(); 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([ expect(store.getLiveBeaconIds()).toEqual([
getBeaconInfoIdentifier(alicesRoom1BeaconInfo), getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
getBeaconInfoIdentifier(alicesRoom2BeaconInfo), getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
@ -214,6 +246,10 @@ describe('OwnBeaconStore', () => {
}); });
it('does geolocation and sends location immediatley when user has live beacons', async () => { it('does geolocation and sends location immediatley when user has live beacons', async () => {
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo, alicesRoom2BeaconInfo,
@ -245,7 +281,8 @@ describe('OwnBeaconStore', () => {
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange])); expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New])); expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([BeaconEvent.Update])); 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 () => { it('destroys beacons', async () => {
@ -270,6 +307,10 @@ describe('OwnBeaconStore', () => {
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
}); });
it('returns true when user has live beacons', async () => { it('returns true when user has live beacons', async () => {
@ -320,6 +361,10 @@ describe('OwnBeaconStore', () => {
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
localStorageGetSpy.mockReturnValue(JSON.stringify([
alicesRoom1BeaconInfo.getId(),
alicesRoom2BeaconInfo.getId(),
]));
}); });
it('returns live beacons when user has live beacons', async () => { it('returns live beacons when user has live beacons', async () => {
@ -371,6 +416,13 @@ describe('OwnBeaconStore', () => {
}); });
describe('on new beacon event', () => { 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 () => { it('ignores events for irrelevant beacons', async () => {
makeRoomsWithStateEvents([]); makeRoomsWithStateEvents([]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
@ -425,6 +477,16 @@ describe('OwnBeaconStore', () => {
}); });
describe('on liveness change event', () => { 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 () => { it('ignores events for irrelevant beacons', async () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
@ -501,6 +563,13 @@ describe('OwnBeaconStore', () => {
}); });
describe('on room membership changes', () => { 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 () => { it('ignores events for rooms without beacons', async () => {
const membershipEvent = makeMembershipEvent(room2Id, aliceId); const membershipEvent = makeMembershipEvent(room2Id, aliceId);
// no beacons for room2 // 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()', () => { describe('stopBeacon()', () => {
beforeEach(() => { beforeEach(() => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
@ -672,9 +789,38 @@ describe('OwnBeaconStore', () => {
expectedUpdateContent, 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', () => { 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 () => { it('stops watching position when user has no more live beacons', async () => {
// geolocation is only going to emit 1 position // geolocation is only going to emit 1 position
geolocation.watchPosition.mockImplementation( geolocation.watchPosition.mockImplementation(
@ -842,6 +988,7 @@ describe('OwnBeaconStore', () => {
// called for each position from watchPosition // called for each position from watchPosition
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5); expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
expect(store.getLiveBeaconIdsWithWireError()).toEqual([]);
expect(store.hasWireErrors()).toBe(false); expect(store.hasWireErrors()).toBe(false);
}); });
@ -892,6 +1039,12 @@ describe('OwnBeaconStore', () => {
// only two allowed failures // only two allowed failures
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2); expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true); 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(store.hasWireErrors()).toBe(true);
expect(emitSpy).toHaveBeenCalledWith( expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo), OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
@ -1055,4 +1208,79 @@ describe('OwnBeaconStore', () => {
expect(mockClient.sendEvent).not.toHaveBeenCalled(); 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();
});
});
}); });

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.