Device manager - logout of other session (PSG-744) (#9280)
* add sign out of current device section in device details * lint * add sign out cta for other sessions * test other device sign out * add pending sign out loader * tidy * fix strict error * use gap instead of nbsp * use more specific assertions in tests, tweak formatting * tweak test
This commit is contained in:
parent
0c22b15bba
commit
10bb10539b
13 changed files with 371 additions and 49 deletions
|
@ -37,6 +37,7 @@ describe('<CurrentDeviceSection />', () => {
|
|||
onVerifyCurrentDevice: jest.fn(),
|
||||
onSignOutCurrentDevice: jest.fn(),
|
||||
isLoading: false,
|
||||
isSigningOut: false,
|
||||
};
|
||||
const getComponent = (props = {}): React.ReactElement =>
|
||||
(<CurrentDeviceSection {...defaultProps} {...props} />);
|
||||
|
|
|
@ -26,6 +26,8 @@ describe('<DeviceDetails />', () => {
|
|||
};
|
||||
const defaultProps = {
|
||||
device: baseDevice,
|
||||
isSigningOut: false,
|
||||
onSignOutDevice: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />;
|
||||
// 14.03.2022 16:15
|
||||
|
@ -60,4 +62,14 @@ describe('<DeviceDetails />', () => {
|
|||
const { container } = render(getComponent({ device }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('disables sign out button while sign out is pending', () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ device, isSigningOut: true }));
|
||||
expect(
|
||||
getByTestId('device-detail-sign-out-cta').getAttribute('aria-disabled'),
|
||||
).toEqual("true");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,7 +43,9 @@ describe('<FilteredDeviceList />', () => {
|
|||
const defaultProps = {
|
||||
onFilterChange: jest.fn(),
|
||||
onDeviceExpandToggle: jest.fn(),
|
||||
onSignOutDevices: jest.fn(),
|
||||
expandedDeviceIds: [],
|
||||
signingOutDeviceIds: [],
|
||||
devices: {
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
|
|
|
@ -110,7 +110,11 @@ HTMLCollection [
|
|||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign out of this session
|
||||
<span
|
||||
class="mx_DeviceDetails_signOutButtonContent"
|
||||
>
|
||||
Sign out of this session
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>,
|
||||
|
|
|
@ -101,6 +101,22 @@ exports[`<DeviceDetails /> renders a verified device 1`] = `
|
|||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_DeviceDetails_signOutButtonContent"
|
||||
>
|
||||
Sign out of this session
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -210,6 +226,22 @@ exports[`<DeviceDetails /> renders device with metadata 1`] = `
|
|||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_DeviceDetails_signOutButtonContent"
|
||||
>
|
||||
Sign out of this session
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -315,6 +347,22 @@ exports[`<DeviceDetails /> renders device without metadata 1`] = `
|
|||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_DeviceDetails_signOutButtonContent"
|
||||
>
|
||||
Sign out of this session
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -21,6 +21,7 @@ import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo';
|
|||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||
import { VerificationRequest } from 'matrix-js-sdk/src/crypto/verification/request/VerificationRequest';
|
||||
import { sleep } from 'matrix-js-sdk/src/utils';
|
||||
|
||||
import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab';
|
||||
import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext';
|
||||
|
@ -31,8 +32,7 @@ import {
|
|||
} from '../../../../../test-utils';
|
||||
import Modal from '../../../../../../src/Modal';
|
||||
import LogoutDialog from '../../../../../../src/components/views/dialogs/LogoutDialog';
|
||||
|
||||
jest.useFakeTimers();
|
||||
import { DeviceWithVerification } from '../../../../../../src/components/views/settings/devices/types';
|
||||
|
||||
describe('<SessionManagerTab />', () => {
|
||||
const aliceId = '@alice:server.org';
|
||||
|
@ -62,6 +62,8 @@ describe('<SessionManagerTab />', () => {
|
|||
getStoredDevice: jest.fn(),
|
||||
getDeviceId: jest.fn().mockReturnValue(deviceId),
|
||||
requestVerification: jest.fn().mockResolvedValue(mockVerificationRequest),
|
||||
deleteMultipleDevices: jest.fn(),
|
||||
generateClientSecret: jest.fn(),
|
||||
});
|
||||
|
||||
const defaultProps = {};
|
||||
|
@ -72,6 +74,16 @@ describe('<SessionManagerTab />', () => {
|
|||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
const toggleDeviceDetails = (
|
||||
getByTestId: ReturnType<typeof render>['getByTestId'],
|
||||
deviceId: DeviceWithVerification['device_id'],
|
||||
) => {
|
||||
// open device detail
|
||||
const tile = getByTestId(`device-tile-${deviceId}`);
|
||||
const toggle = tile.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(logger, 'error').mockRestore();
|
||||
|
@ -83,6 +95,10 @@ describe('<SessionManagerTab />', () => {
|
|||
mockCrossSigningInfo.checkDeviceTrust
|
||||
.mockReset()
|
||||
.mockReturnValue(new DeviceTrustLevel(false, false, false, false));
|
||||
|
||||
mockClient.getDevices
|
||||
.mockReset()
|
||||
.mockResolvedValue({ devices: [alicesMobileDevice] });
|
||||
});
|
||||
|
||||
it('renders spinner while devices load', () => {
|
||||
|
@ -257,24 +273,18 @@ describe('<SessionManagerTab />', () => {
|
|||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
const tile1 = getByTestId(`device-tile-${alicesOlderMobileDevice.device_id}`);
|
||||
const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle1);
|
||||
toggleDeviceDetails(getByTestId, alicesOlderMobileDevice.device_id);
|
||||
|
||||
// device details are expanded
|
||||
expect(getByTestId(`device-detail-${alicesOlderMobileDevice.device_id}`)).toBeTruthy();
|
||||
|
||||
const tile2 = getByTestId(`device-tile-${alicesMobileDevice.device_id}`);
|
||||
const toggle2 = tile2.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle2);
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
// both device details are expanded
|
||||
expect(getByTestId(`device-detail-${alicesOlderMobileDevice.device_id}`)).toBeTruthy();
|
||||
expect(getByTestId(`device-detail-${alicesMobileDevice.device_id}`)).toBeTruthy();
|
||||
|
||||
const tile3 = getByTestId(`device-tile-${alicesMobileDevice.device_id}`);
|
||||
const toggle3 = tile3.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle3);
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
// alicesMobileDevice was toggled off
|
||||
expect(queryByTestId(`device-detail-${alicesMobileDevice.device_id}`)).toBeFalsy();
|
||||
|
@ -294,9 +304,7 @@ describe('<SessionManagerTab />', () => {
|
|||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
const tile1 = getByTestId(`device-tile-${alicesOlderMobileDevice.device_id}`);
|
||||
const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle1);
|
||||
toggleDeviceDetails(getByTestId, alicesOlderMobileDevice.device_id);
|
||||
|
||||
// verify device button is not rendered
|
||||
expect(queryByTestId(`verification-status-button-${alicesOlderMobileDevice.device_id}`)).toBeFalsy();
|
||||
|
@ -323,9 +331,7 @@ describe('<SessionManagerTab />', () => {
|
|||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
const tile1 = getByTestId(`device-tile-${alicesMobileDevice.device_id}`);
|
||||
const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle1);
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
// click verify button from current session section
|
||||
fireEvent.click(getByTestId(`verification-status-button-${alicesMobileDevice.device_id}`));
|
||||
|
@ -355,9 +361,7 @@ describe('<SessionManagerTab />', () => {
|
|||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
const tile1 = getByTestId(`device-tile-${alicesMobileDevice.device_id}`);
|
||||
const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle1);
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
// reset mock counter before triggering verification
|
||||
mockClient.getDevices.mockClear();
|
||||
|
@ -387,10 +391,7 @@ describe('<SessionManagerTab />', () => {
|
|||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
// open device detail
|
||||
const tile1 = getByTestId(`device-tile-${alicesDevice.device_id}`);
|
||||
const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||
fireEvent.click(toggle1);
|
||||
toggleDeviceDetails(getByTestId, alicesDevice.device_id);
|
||||
|
||||
const signOutButton = getByTestId('device-detail-sign-out-cta');
|
||||
expect(signOutButton).toMatchSnapshot();
|
||||
|
@ -399,5 +400,165 @@ describe('<SessionManagerTab />', () => {
|
|||
// logout dialog opened
|
||||
expect(modalSpy).toHaveBeenCalledWith(LogoutDialog, {}, undefined, false, true);
|
||||
});
|
||||
|
||||
describe('other devices', () => {
|
||||
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } };
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.deleteMultipleDevices.mockReset();
|
||||
});
|
||||
|
||||
it('deletes a device when interactive auth is not required', async () => {
|
||||
mockClient.deleteMultipleDevices.mockResolvedValue({});
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({ devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice] })
|
||||
// pretend it was really deleted on refresh
|
||||
.mockResolvedValueOnce({ devices: [alicesDevice, alicesOlderMobileDevice] });
|
||||
|
||||
const { getByTestId } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`);
|
||||
const signOutButton = deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element;
|
||||
fireEvent.click(signOutButton);
|
||||
|
||||
// sign out button is disabled with spinner
|
||||
expect((deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element).getAttribute('aria-disabled')).toEqual("true");
|
||||
// delete called
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
|
||||
[alicesMobileDevice.device_id], undefined,
|
||||
);
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// devices refreshed
|
||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes a device when interactive auth is required', async () => {
|
||||
mockClient.deleteMultipleDevices
|
||||
// require auth
|
||||
.mockRejectedValueOnce(interactiveAuthError)
|
||||
// then succeed
|
||||
.mockResolvedValueOnce({});
|
||||
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({ devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice] })
|
||||
// pretend it was really deleted on refresh
|
||||
.mockResolvedValueOnce({ devices: [alicesDevice, alicesOlderMobileDevice] });
|
||||
|
||||
const { getByTestId, getByLabelText } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
// reset mock count after initial load
|
||||
mockClient.getDevices.mockClear();
|
||||
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`);
|
||||
const signOutButton = deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element;
|
||||
fireEvent.click(signOutButton);
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
// modal rendering has some weird sleeps
|
||||
await sleep(100);
|
||||
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
|
||||
[alicesMobileDevice.device_id], undefined,
|
||||
);
|
||||
|
||||
const modal = document.getElementsByClassName('mx_Dialog');
|
||||
expect(modal.length).toBeTruthy();
|
||||
|
||||
// fill password and submit for interactive auth
|
||||
act(() => {
|
||||
fireEvent.change(getByLabelText('Password'), { target: { value: 'topsecret' } });
|
||||
fireEvent.submit(getByLabelText('Password'));
|
||||
});
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// called again with auth
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([alicesMobileDevice.device_id],
|
||||
{ identifier: {
|
||||
type: "m.id.user", user: aliceId,
|
||||
}, password: "", type: "m.login.password", user: aliceId,
|
||||
});
|
||||
// devices refreshed
|
||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears loading state when device deletion is cancelled during interactive auth', async () => {
|
||||
mockClient.deleteMultipleDevices
|
||||
// require auth
|
||||
.mockRejectedValueOnce(interactiveAuthError)
|
||||
// then succeed
|
||||
.mockResolvedValueOnce({});
|
||||
|
||||
mockClient.getDevices
|
||||
.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice] });
|
||||
|
||||
const { getByTestId, getByLabelText } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`);
|
||||
const signOutButton = deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element;
|
||||
fireEvent.click(signOutButton);
|
||||
|
||||
// button is loading
|
||||
expect((deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element).getAttribute('aria-disabled')).toEqual("true");
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
// modal rendering has some weird sleeps
|
||||
await sleep(100);
|
||||
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
|
||||
[alicesMobileDevice.device_id], undefined,
|
||||
);
|
||||
|
||||
const modal = document.getElementsByClassName('mx_Dialog');
|
||||
expect(modal.length).toBeTruthy();
|
||||
|
||||
// cancel iau by closing modal
|
||||
act(() => {
|
||||
fireEvent.click(getByLabelText('Close dialog'));
|
||||
});
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// not called again
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledTimes(1);
|
||||
// devices not refreshed (not called since initial fetch)
|
||||
expect(mockClient.getDevices).toHaveBeenCalledTimes(1);
|
||||
|
||||
// loading state cleared
|
||||
expect((deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element).getAttribute('aria-disabled')).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,11 @@ exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
|
|||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign out of this session
|
||||
<span
|
||||
class="mx_DeviceDetails_signOutButtonContent"
|
||||
>
|
||||
Sign out of this session
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue