2022-10-19 14:31:20 +02:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
import { cleanup, render, waitFor } from '@testing-library/react';
|
2022-10-19 14:31:20 +02:00
|
|
|
import { mocked } from 'jest-mock';
|
|
|
|
import React from 'react';
|
|
|
|
import { MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
import LoginWithQR, { Click, Mode, Phase } from '../../../../../src/components/views/auth/LoginWithQR';
|
2022-10-19 14:31:20 +02:00
|
|
|
import type { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
|
|
|
|
|
|
|
jest.mock('matrix-js-sdk/src/rendezvous');
|
|
|
|
jest.mock('matrix-js-sdk/src/rendezvous/transports');
|
|
|
|
jest.mock('matrix-js-sdk/src/rendezvous/channels');
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
const mockedFlow = jest.fn();
|
|
|
|
|
|
|
|
jest.mock('../../../../../src/components/views/auth/LoginWithQRFlow', () => (props) => {
|
|
|
|
mockedFlow(props);
|
|
|
|
return <div />;
|
|
|
|
});
|
|
|
|
|
2022-10-19 14:31:20 +02:00
|
|
|
function makeClient() {
|
|
|
|
return mocked({
|
|
|
|
getUser: jest.fn(),
|
|
|
|
isGuest: jest.fn().mockReturnValue(false),
|
|
|
|
isUserIgnored: jest.fn(),
|
|
|
|
isCryptoEnabled: jest.fn(),
|
|
|
|
getUserId: jest.fn(),
|
|
|
|
on: jest.fn(),
|
|
|
|
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
|
|
|
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
|
|
|
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
|
|
|
|
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(true),
|
|
|
|
removeListener: jest.fn(),
|
|
|
|
requestLoginToken: jest.fn(),
|
|
|
|
currentState: {
|
|
|
|
on: jest.fn(),
|
|
|
|
},
|
|
|
|
} as unknown as MatrixClient);
|
|
|
|
}
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
function unresolvedPromise<T>(): Promise<T> {
|
|
|
|
return new Promise(() => {});
|
|
|
|
}
|
|
|
|
|
2022-10-19 14:31:20 +02:00
|
|
|
describe('<LoginWithQR />', () => {
|
2022-11-02 11:51:20 +01:00
|
|
|
let client = makeClient();
|
2022-10-19 14:31:20 +02:00
|
|
|
const defaultProps = {
|
|
|
|
mode: Mode.Show,
|
|
|
|
onFinished: jest.fn(),
|
|
|
|
};
|
|
|
|
const mockConfirmationDigits = 'mock-confirmation-digits';
|
2022-11-02 11:51:20 +01:00
|
|
|
const mockRendezvousCode = 'mock-rendezvous-code';
|
2022-10-19 14:31:20 +02:00
|
|
|
const newDeviceId = 'new-device-id';
|
|
|
|
|
|
|
|
const getComponent = (props: { client: MatrixClient, onFinished?: () => void }) =>
|
2022-11-02 11:51:20 +01:00
|
|
|
(<React.StrictMode><LoginWithQR {...defaultProps} {...props} /></React.StrictMode>);
|
2022-10-19 14:31:20 +02:00
|
|
|
|
|
|
|
beforeEach(() => {
|
2022-11-02 11:51:20 +01:00
|
|
|
mockedFlow.mockReset();
|
|
|
|
jest.resetAllMocks();
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockResolvedValue();
|
|
|
|
// @ts-ignore
|
|
|
|
// workaround for https://github.com/facebook/jest/issues/9675
|
|
|
|
MSC3906Rendezvous.prototype.code = mockRendezvousCode;
|
2022-10-19 14:31:20 +02:00
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'cancel').mockResolvedValue();
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockResolvedValue(mockConfirmationDigits);
|
2022-11-02 11:51:20 +01:00
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'declineLoginOnExistingDevice').mockResolvedValue();
|
2022-10-19 14:31:20 +02:00
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockResolvedValue(newDeviceId);
|
2022-11-02 11:51:20 +01:00
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'verifyNewDeviceOnExistingDevice').mockResolvedValue(undefined);
|
2022-10-19 14:31:20 +02:00
|
|
|
client.requestLoginToken.mockResolvedValue({
|
|
|
|
login_token: 'token',
|
|
|
|
expires_in: 1000,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
afterEach(() => {
|
|
|
|
client = makeClient();
|
|
|
|
jest.clearAllMocks();
|
|
|
|
jest.useRealTimers();
|
|
|
|
cleanup();
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('no homeserver support', async () => {
|
|
|
|
// simulate no support
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRejectedValue('');
|
|
|
|
render(getComponent({ client }));
|
|
|
|
await waitFor(() =>
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.Error,
|
|
|
|
failureReason: RendezvousFailureReason.HomeserverLacksSupport,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
}),
|
|
|
|
);
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('failed to connect', async () => {
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockRejectedValue('');
|
|
|
|
render(getComponent({ client }));
|
|
|
|
await waitFor(() =>
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.Error,
|
|
|
|
failureReason: RendezvousFailureReason.Unknown,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
}),
|
|
|
|
);
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('render QR then cancel and try again', async () => {
|
|
|
|
const onFinished = jest.fn();
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockImplementation(() => unresolvedPromise());
|
|
|
|
render(getComponent({ client, onFinished }));
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.ShowingQR,
|
|
|
|
})));
|
|
|
|
// display QR code
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.ShowingQR,
|
|
|
|
code: mockRendezvousCode,
|
|
|
|
onClick: expect.any(Function),
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// cancel
|
|
|
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
|
|
|
await onClick(Click.Cancel);
|
|
|
|
expect(onFinished).toHaveBeenCalledWith(false);
|
|
|
|
expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled);
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// try again
|
|
|
|
onClick(Click.TryAgain);
|
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.ShowingQR,
|
|
|
|
})));
|
|
|
|
// display QR code
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.ShowingQR,
|
|
|
|
code: mockRendezvousCode,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
});
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('render QR then back', async () => {
|
|
|
|
const onFinished = jest.fn();
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockReturnValue(unresolvedPromise());
|
|
|
|
render(getComponent({ client, onFinished }));
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.ShowingQR,
|
|
|
|
})));
|
|
|
|
// display QR code
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.ShowingQR,
|
|
|
|
code: mockRendezvousCode,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
});
|
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// back
|
|
|
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
|
|
|
await onClick(Click.Back);
|
|
|
|
expect(onFinished).toHaveBeenCalledWith(false);
|
|
|
|
expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled);
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('render QR then decline', async () => {
|
|
|
|
const onFinished = jest.fn();
|
|
|
|
render(getComponent({ client, onFinished }));
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
})));
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
confirmationDigits: mockConfirmationDigits,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
});
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// decline
|
|
|
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
|
|
|
await onClick(Click.Decline);
|
|
|
|
expect(onFinished).toHaveBeenCalledWith(false);
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('approve - no crypto', async () => {
|
|
|
|
// @ts-ignore
|
|
|
|
client.crypto = undefined;
|
2022-10-19 14:31:20 +02:00
|
|
|
const onFinished = jest.fn();
|
2022-11-02 11:51:20 +01:00
|
|
|
// jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockReturnValue(unresolvedPromise());
|
|
|
|
render(getComponent({ client, onFinished }));
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
})));
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
confirmationDigits: mockConfirmationDigits,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
});
|
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// approve
|
|
|
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
|
|
|
await onClick(Click.Approve);
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.WaitingForDevice,
|
|
|
|
})));
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(onFinished).toHaveBeenCalledWith(true);
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('approve + verifying', async () => {
|
|
|
|
const onFinished = jest.fn();
|
|
|
|
// @ts-ignore
|
|
|
|
client.crypto = {};
|
|
|
|
jest.spyOn(MSC3906Rendezvous.prototype, 'verifyNewDeviceOnExistingDevice')
|
|
|
|
.mockImplementation(() => unresolvedPromise());
|
|
|
|
render(getComponent({ client, onFinished }));
|
2022-10-19 14:31:20 +02:00
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
})));
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
confirmationDigits: mockConfirmationDigits,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
});
|
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// approve
|
|
|
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
|
|
|
onClick(Click.Approve);
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.Verifying,
|
|
|
|
})));
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
|
|
|
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
|
|
|
// expect(onFinished).toHaveBeenCalledWith(true);
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
test('approve + verify', async () => {
|
|
|
|
const onFinished = jest.fn();
|
2022-10-19 14:31:20 +02:00
|
|
|
// @ts-ignore
|
|
|
|
client.crypto = {};
|
2022-11-02 11:51:20 +01:00
|
|
|
render(getComponent({ client, onFinished }));
|
|
|
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
})));
|
|
|
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
|
|
|
phase: Phase.Connected,
|
|
|
|
confirmationDigits: mockConfirmationDigits,
|
|
|
|
onClick: expect.any(Function),
|
|
|
|
});
|
|
|
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
|
|
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
2022-10-19 14:31:20 +02:00
|
|
|
|
2022-11-02 11:51:20 +01:00
|
|
|
// approve
|
|
|
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
|
|
|
await onClick(Click.Approve);
|
|
|
|
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
2022-10-19 14:31:20 +02:00
|
|
|
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
2022-11-02 11:51:20 +01:00
|
|
|
expect(onFinished).toHaveBeenCalledWith(true);
|
2022-10-19 14:31:20 +02:00
|
|
|
});
|
|
|
|
});
|