MSC4108 support OIDC QR code login (#12370)
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
This commit is contained in:
parent
ca7760789b
commit
1677ed1be0
24 changed files with 1558 additions and 733 deletions
|
@ -17,7 +17,13 @@ limitations under the License.
|
|||
import { cleanup, render, waitFor } from "@testing-library/react";
|
||||
import { MockedObject, mocked } from "jest-mock";
|
||||
import React from "react";
|
||||
import { MSC3906Rendezvous, LegacyRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
LegacyRendezvousFailureReason,
|
||||
ClientRendezvousFailureReason,
|
||||
MSC4108SignInWithQR,
|
||||
MSC4108FailureReason,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import LoginWithQR from "../../../../../src/components/views/auth/LoginWithQR";
|
||||
|
@ -65,6 +71,7 @@ function unresolvedPromise<T>(): Promise<T> {
|
|||
describe("<LoginWithQR />", () => {
|
||||
let client!: MockedObject<MatrixClient>;
|
||||
const defaultProps = {
|
||||
legacy: true,
|
||||
mode: Mode.Show,
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
@ -72,29 +79,10 @@ describe("<LoginWithQR />", () => {
|
|||
const mockRendezvousCode = "mock-rendezvous-code";
|
||||
const newDeviceId = "new-device-id";
|
||||
|
||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
||||
<React.StrictMode>
|
||||
<LoginWithQR {...defaultProps} {...props} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockedFlow.mockReset();
|
||||
jest.resetAllMocks();
|
||||
client = makeClient();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue();
|
||||
// @ts-ignore
|
||||
// workaround for https://github.com/facebook/jest/issues/9675
|
||||
MSC3906Rendezvous.prototype.code = mockRendezvousCode;
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined);
|
||||
client.requestLoginToken.mockResolvedValue({
|
||||
login_token: "token",
|
||||
expires_in_ms: 1000 * 1000,
|
||||
} as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -104,279 +92,374 @@ describe("<LoginWithQR />", () => {
|
|||
cleanup();
|
||||
});
|
||||
|
||||
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: LegacyRendezvousFailureReason.HomeserverLacksSupport,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
describe("MSC3906", () => {
|
||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
||||
<React.StrictMode>
|
||||
<LoginWithQR {...defaultProps} {...props} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("failed to connect", async () => {
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue("");
|
||||
render(getComponent({ client }));
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.Error,
|
||||
failureReason: LegacyRendezvousFailureReason.Unknown,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("render QR then cancel and try again", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockImplementation(() => unresolvedPromise());
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// cancel
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Cancel);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
|
||||
|
||||
// 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),
|
||||
});
|
||||
});
|
||||
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise());
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// back
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Back);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("render QR then decline", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Connected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.Connected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
beforeEach(() => {
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue();
|
||||
// @ts-ignore
|
||||
// workaround for https://github.com/facebook/jest/issues/9675
|
||||
MSC3906Rendezvous.prototype.code = mockRendezvousCode;
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined);
|
||||
client.requestLoginToken.mockResolvedValue({
|
||||
login_token: "token",
|
||||
expires_in_ms: 1000 * 1000,
|
||||
} as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet
|
||||
});
|
||||
|
||||
// decline
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Decline);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("approve - no crypto", async () => {
|
||||
(client as any).crypto = undefined;
|
||||
(client as any).getCrypto = () => undefined;
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.WaitingForDevice,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve + verifying", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() =>
|
||||
unresolvedPromise(),
|
||||
);
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Verifying,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
// expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve + verify", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
expect(rendezvous.close).toHaveBeenCalled();
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve - rate limited", async () => {
|
||||
mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429));
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
// the 429 error should be handled and mapped
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
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: "rate_limited",
|
||||
failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("failed to connect", async () => {
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue("");
|
||||
render(getComponent({ client }));
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.Error,
|
||||
failureReason: ClientRendezvousFailureReason.Unknown,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise());
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
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();
|
||||
|
||||
// back
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Back);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("render QR then decline", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
|
||||
// decline
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Decline);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("approve - no crypto", async () => {
|
||||
(client as any).crypto = undefined;
|
||||
(client as any).getCrypto = () => undefined;
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.WaitingForDevice,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve + verifying", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() =>
|
||||
unresolvedPromise(),
|
||||
);
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Verifying,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
// expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve + verify", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
expect(rendezvous.close).toHaveBeenCalled();
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve - rate limited", async () => {
|
||||
mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429));
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
// the 429 error should be handled and mapped
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Error,
|
||||
failureReason: "rate_limited",
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MSC4108", () => {
|
||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
||||
<React.StrictMode>
|
||||
<LoginWithQR {...defaultProps} {...props} legacy={false} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||
render(getComponent({ client, onFinished }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||
|
||||
// back
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Back);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("failed to connect", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockRejectedValue(
|
||||
new HTTPError("Internal Server Error", 500),
|
||||
);
|
||||
const fn = jest.spyOn(MSC4108SignInWithQR.prototype, "cancel");
|
||||
await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown));
|
||||
});
|
||||
|
||||
test("reciprocates login", async () => {
|
||||
jest.spyOn(global.window, "open");
|
||||
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
|
||||
verificationUri: "mock-verification-uri",
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.WaitingForDevice,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
|
||||
});
|
||||
|
||||
test("handles errors during reciprocation", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockRejectedValue(
|
||||
new HTTPError("Internal Server Error", 500),
|
||||
);
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Error,
|
||||
failureReason: ClientRendezvousFailureReason.Unknown,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("handles user cancelling during reciprocation", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Cancel);
|
||||
|
||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue