Refactor QR link flow to use new SDK methods (#33309)
* Refactor QR link flow to use new SDK methods Requires https://github.com/matrix-org/matrix-js-sdk/pull/5283 For element-hq/wat-internal#188 Split out from https://github.com/element-hq/element-web/pull/33184 * Link to js-sdk branch * Update tests * Simplify * Revert js-sdk linking * Use js-sdk isSignInWithQRAvailable API to simplify code
This commit is contained in:
@@ -9,9 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
ClientRendezvousFailureReason,
|
ClientRendezvousFailureReason,
|
||||||
|
linkNewDeviceByGeneratingQR,
|
||||||
MSC4108FailureReason,
|
MSC4108FailureReason,
|
||||||
MSC4108RendezvousSession,
|
|
||||||
MSC4108SecureChannel,
|
|
||||||
MSC4108SignInWithQR,
|
MSC4108SignInWithQR,
|
||||||
RendezvousError,
|
RendezvousError,
|
||||||
type RendezvousFailureReason,
|
type RendezvousFailureReason,
|
||||||
@@ -55,6 +54,7 @@ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;
|
|||||||
*/
|
*/
|
||||||
export default class LoginWithQR extends React.Component<IProps, IState> {
|
export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||||
private finished = false;
|
private finished = false;
|
||||||
|
private abortController?: AbortController;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -69,35 +69,31 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.updateMode(this.props.mode).then(() => {});
|
void this.updateMode(this.props.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||||
if (prevProps.mode !== this.props.mode) {
|
if (prevProps.mode !== this.props.mode) {
|
||||||
this.updateMode(this.props.mode).then(() => {});
|
void this.updateMode(this.props.mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateMode(mode: Mode, showLoading = true): Promise<void> {
|
private async updateMode(mode: Mode, showLoading = true): Promise<void> {
|
||||||
if (this.state.rendezvous) {
|
this.abortController?.abort();
|
||||||
const rendezvous = this.state.rendezvous;
|
this.abortController = new AbortController();
|
||||||
rendezvous.onFailure = undefined;
|
this.setState({ rendezvous: undefined });
|
||||||
this.setState({ rendezvous: undefined });
|
|
||||||
}
|
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
this.setState({ phase: Phase.Loading });
|
this.setState({ phase: Phase.Loading });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === Mode.Show) {
|
if (mode === Mode.Show) {
|
||||||
await this.generateAndShowCode();
|
await this.generateAndShowCode(this.abortController);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
if (this.state.rendezvous && !this.finished) {
|
if (!this.finished) {
|
||||||
// eslint-disable-next-line react/no-direct-mutation-state
|
this.abortController?.abort();
|
||||||
this.state.rendezvous.onFailure = undefined;
|
|
||||||
// calling cancel will call close() as well to clean up the resources
|
|
||||||
this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,24 +102,18 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||||||
this.props.onFinished(success);
|
this.props.onFinished(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateAndShowCode = async (): Promise<void> => {
|
private generateAndShowCode = async (abortController: AbortController): Promise<void> => {
|
||||||
let rendezvous: MSC4108SignInWithQR;
|
let rendezvous: MSC4108SignInWithQR;
|
||||||
try {
|
try {
|
||||||
const transport = new MSC4108RendezvousSession({
|
rendezvous = await linkNewDeviceByGeneratingQR(this.props.client, this.onFailure, abortController.signal);
|
||||||
onFailure: this.onFailure,
|
if (abortController.signal.aborted) return;
|
||||||
client: this.props.client,
|
|
||||||
});
|
|
||||||
await transport.send("");
|
|
||||||
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);
|
|
||||||
rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure);
|
|
||||||
|
|
||||||
await rendezvous.generateCode();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: Phase.ShowingQR,
|
phase: Phase.ShowingQR,
|
||||||
rendezvous,
|
rendezvous,
|
||||||
failureReason: undefined,
|
failureReason: undefined,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
logger.error("Error whilst generating QR code", e);
|
logger.error("Error whilst generating QR code", e);
|
||||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.HomeserverLacksSupport });
|
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.HomeserverLacksSupport });
|
||||||
return;
|
return;
|
||||||
@@ -142,8 +132,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
// we ask the user to confirm that the channel is secure
|
// we ask the user to confirm that the channel is secure
|
||||||
} catch (e: RendezvousError | unknown) {
|
} catch (e: RendezvousError | unknown) {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
logger.error("Error whilst approving login", e);
|
logger.error("Error whilst approving login", e);
|
||||||
await rendezvous?.cancel(
|
await rendezvous.cancel(
|
||||||
e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown,
|
e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -210,6 +201,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
|
this.abortController?.abort();
|
||||||
this.setState({
|
this.setState({
|
||||||
rendezvous: undefined,
|
rendezvous: undefined,
|
||||||
verificationUri: undefined,
|
verificationUri: undefined,
|
||||||
|
|||||||
@@ -7,48 +7,35 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
type IServerVersions,
|
|
||||||
type OidcClientConfig,
|
|
||||||
type MatrixClient,
|
|
||||||
DEVICE_CODE_SCOPE,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code";
|
import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code";
|
||||||
import { Text } from "@vector-im/compound-web";
|
import { Text } from "@vector-im/compound-web";
|
||||||
|
import { isSignInWithQRAvailable } from "matrix-js-sdk/src/rendezvous";
|
||||||
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
|
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onShowQr: () => void;
|
onShowQr: () => void;
|
||||||
versions?: IServerVersions;
|
|
||||||
oidcClientConfig?: OidcClientConfig;
|
|
||||||
isCrossSigningReady?: boolean;
|
isCrossSigningReady?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldShowQr(
|
export async function shouldShowQrForLinkNewDevice(cli: MatrixClient, isCrossSigningReady: boolean): Promise<boolean> {
|
||||||
cli: MatrixClient,
|
const doesServerHaveSupport = await isSignInWithQRAvailable(cli);
|
||||||
isCrossSigningReady: boolean,
|
|
||||||
oidcClientConfig?: OidcClientConfig,
|
|
||||||
versions?: IServerVersions,
|
|
||||||
): boolean {
|
|
||||||
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
|
|
||||||
|
|
||||||
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
|
return doesServerHaveSupport && !!cli.getCrypto()?.exportSecretsBundle && isCrossSigningReady;
|
||||||
|
|
||||||
return (
|
|
||||||
!!deviceAuthorizationGrantSupported &&
|
|
||||||
msc4108Supported &&
|
|
||||||
!!cli.getCrypto()?.exportSecretsBundle &&
|
|
||||||
isCrossSigningReady
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => {
|
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, isCrossSigningReady }) => {
|
||||||
const cli = useMatrixClientContext();
|
const cli = useMatrixClientContext();
|
||||||
const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions);
|
const offerShowQr = useAsyncMemo(
|
||||||
|
() => shouldShowQrForLinkNewDevice(cli, !!isCrossSigningReady),
|
||||||
|
[cli, isCrossSigningReady],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
|
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
|
||||||
|
|||||||
@@ -162,14 +162,6 @@ const SessionManagerTab: React.FC<{
|
|||||||
const disableMultipleSignout = !!accountManagement?.endpoint;
|
const disableMultipleSignout = !!accountManagement?.endpoint;
|
||||||
const userId = matrixClient?.getUserId();
|
const userId = matrixClient?.getUserId();
|
||||||
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
||||||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
|
||||||
const oidcClientConfig = useAsyncMemo(async () => {
|
|
||||||
try {
|
|
||||||
return await matrixClient?.getAuthMetadata();
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to discover OIDC metadata", e);
|
|
||||||
}
|
|
||||||
}, [matrixClient]);
|
|
||||||
const isCrossSigningReady = useAsyncMemo(
|
const isCrossSigningReady = useAsyncMemo(
|
||||||
async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false,
|
async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false,
|
||||||
[matrixClient],
|
[matrixClient],
|
||||||
@@ -279,12 +271,7 @@ const SessionManagerTab: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<SettingsTab>
|
<SettingsTab>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<LoginWithQRSection
|
<LoginWithQRSection onShowQr={onShowQrClicked} isCrossSigningReady={isCrossSigningReady} />
|
||||||
onShowQr={onShowQrClicked}
|
|
||||||
versions={clientVersions}
|
|
||||||
oidcClientConfig={oidcClientConfig}
|
|
||||||
isCrossSigningReady={isCrossSigningReady}
|
|
||||||
/>
|
|
||||||
<SecurityRecommendations
|
<SecurityRecommendations
|
||||||
devices={devices}
|
devices={devices}
|
||||||
goToFilteredList={onGoToFilteredList}
|
goToFilteredList={onGoToFilteredList}
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { cleanup, render, waitFor } from "jest-matrix-react";
|
import { cleanup, render, waitFor } from "jest-matrix-react";
|
||||||
import { mocked, type MockedObject } from "jest-mock";
|
import { mocked, type MockedObject } from "jest-mock";
|
||||||
import React from "react";
|
import React, { createRef, type RefObject } from "react";
|
||||||
import {
|
import {
|
||||||
ClientRendezvousFailureReason,
|
ClientRendezvousFailureReason,
|
||||||
MSC4108FailureReason,
|
MSC4108FailureReason,
|
||||||
MSC4108SignInWithQR,
|
MSC4108SignInWithQR,
|
||||||
RendezvousError,
|
RendezvousError,
|
||||||
} from "matrix-js-sdk/src/rendezvous";
|
} from "matrix-js-sdk/src/rendezvous";
|
||||||
import { HTTPError, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { HTTPError, type MatrixClient, MatrixHttpApi } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import LoginWithQR, { LoginWithQRFailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
|
import LoginWithQR, { LoginWithQRFailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
|
||||||
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
|
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
|
||||||
|
|
||||||
jest.mock("matrix-js-sdk/src/rendezvous");
|
|
||||||
jest.mock("matrix-js-sdk/src/rendezvous/transports");
|
jest.mock("matrix-js-sdk/src/rendezvous/transports");
|
||||||
jest.mock("matrix-js-sdk/src/rendezvous/channels");
|
jest.mock("matrix-js-sdk/src/rendezvous/channels");
|
||||||
|
jest.mock("matrix-js-sdk/src/rendezvous/channels/MSC4108SecureChannel.ts");
|
||||||
|
|
||||||
const mockedFlow = jest.fn();
|
const mockedFlow = jest.fn();
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ jest.mock("../../../../../../src/components/views/auth/LoginWithQRFlow", () => (
|
|||||||
});
|
});
|
||||||
|
|
||||||
function makeClient() {
|
function makeClient() {
|
||||||
return mocked({
|
const cli = mocked({
|
||||||
getUser: jest.fn(),
|
getUser: jest.fn(),
|
||||||
isGuest: jest.fn().mockReturnValue(false),
|
isGuest: jest.fn().mockReturnValue(false),
|
||||||
isUserIgnored: jest.fn(),
|
isUserIgnored: jest.fn(),
|
||||||
@@ -49,7 +49,16 @@ function makeClient() {
|
|||||||
},
|
},
|
||||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||||
getCrypto: jest.fn().mockReturnValue({}),
|
getCrypto: jest.fn().mockReturnValue({}),
|
||||||
|
getDomain: jest.fn(),
|
||||||
} as unknown as MatrixClient);
|
} as unknown as MatrixClient);
|
||||||
|
|
||||||
|
cli.http = new MatrixHttpApi(cli, {
|
||||||
|
baseUrl: "https://server/",
|
||||||
|
prefix: "prefix",
|
||||||
|
onlyData: true,
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
return cli;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unresolvedPromise<T>(): Promise<T> {
|
function unresolvedPromise<T>(): Promise<T> {
|
||||||
@@ -62,13 +71,12 @@ describe("<LoginWithQR />", () => {
|
|||||||
legacy: true,
|
legacy: true,
|
||||||
mode: Mode.Show,
|
mode: Mode.Show,
|
||||||
onFinished: jest.fn(),
|
onFinished: jest.fn(),
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockedFlow.mockReset();
|
mockedFlow.mockReset();
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
client = makeClient();
|
client = makeClient();
|
||||||
jest.useFakeTimers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -79,14 +87,20 @@ describe("<LoginWithQR />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("MSC4108", () => {
|
describe("MSC4108", () => {
|
||||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
const getComponent = (props: {
|
||||||
<LoginWithQR {...defaultProps} {...props} />
|
client: MatrixClient;
|
||||||
);
|
onFinished?: () => void;
|
||||||
|
ref?: RefObject<LoginWithQR | null>;
|
||||||
|
}) => <LoginWithQR {...defaultProps} {...props} />;
|
||||||
|
|
||||||
test("render QR then back", async () => {
|
test("render QR then back", async () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||||
render(getComponent({ client, onFinished }));
|
jest.spyOn(MSC4108SignInWithQR.prototype, "generateCode");
|
||||||
|
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols");
|
||||||
|
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel");
|
||||||
|
const ref = createRef<LoginWithQR>();
|
||||||
|
render(getComponent({ client, onFinished, ref }));
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
@@ -95,7 +109,7 @@ describe("<LoginWithQR />", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
const rendezvous = ref.current!.state.rendezvous!;
|
||||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -109,7 +123,8 @@ describe("<LoginWithQR />", () => {
|
|||||||
test("should open a new channel if expires before qr scan", async () => {
|
test("should open a new channel if expires before qr scan", async () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||||
render(getComponent({ client, onFinished }));
|
const ref = createRef<LoginWithQR>();
|
||||||
|
render(getComponent({ client, onFinished, ref }));
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
@@ -118,15 +133,15 @@ describe("<LoginWithQR />", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
const rendezvous = ref.current!.state.rendezvous!;
|
||||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||||
|
|
||||||
// Expire the channel
|
// Expire the channel
|
||||||
const onFailure = mocked(MSC4108SignInWithQR).mock.calls[0][3];
|
rendezvous.onFailure!(ClientRendezvousFailureReason.Expired);
|
||||||
onFailure!(ClientRendezvousFailureReason.Expired);
|
|
||||||
await jest.runAllTimersAsync();
|
await jest.runAllTimersAsync();
|
||||||
await waitFor(() => expect(mocked(MSC4108SignInWithQR).mock.instances).toHaveLength(2));
|
await waitFor(() => expect(ref.current!.state.rendezvous).toBeDefined());
|
||||||
|
expect(ref.current!.state.rendezvous).not.toBe(rendezvous);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("failed to connect", async () => {
|
test("failed to connect", async () => {
|
||||||
@@ -168,9 +183,11 @@ describe("<LoginWithQR />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("reciprocates login", async () => {
|
test("reciprocates login", async () => {
|
||||||
|
const ref = createRef<LoginWithQR>();
|
||||||
jest.spyOn(global.window, "open");
|
jest.spyOn(global.window, "open");
|
||||||
|
|
||||||
render(getComponent({ client }));
|
render(getComponent({ client, ref }));
|
||||||
|
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockResolvedValue({});
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
|
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
|
||||||
verificationUri: "mock-verification-uri",
|
verificationUri: "mock-verification-uri",
|
||||||
@@ -193,10 +210,14 @@ describe("<LoginWithQR />", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
|
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
|
||||||
|
|
||||||
|
const rendezvous = ref.current!.state.rendezvous!;
|
||||||
|
expect(rendezvous.shareSecrets).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles errors during protocol negotiation", async () => {
|
test("handles errors during protocol negotiation", async () => {
|
||||||
render(getComponent({ client }));
|
const ref = createRef<LoginWithQR>();
|
||||||
|
render(getComponent({ client, ref }));
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
||||||
const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol);
|
const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol);
|
||||||
// @ts-ignore work-around for lazy mocks
|
// @ts-ignore work-around for lazy mocks
|
||||||
@@ -211,7 +232,7 @@ describe("<LoginWithQR />", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
const rendezvous = ref.current!.state.rendezvous!;
|
||||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
|
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -244,7 +265,8 @@ describe("<LoginWithQR />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("handles user cancelling during reciprocation", async () => {
|
test("handles user cancelling during reciprocation", async () => {
|
||||||
render(getComponent({ client }));
|
const ref = createRef<LoginWithQR>();
|
||||||
|
render(getComponent({ client, ref }));
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||||
@@ -259,7 +281,7 @@ describe("<LoginWithQR />", () => {
|
|||||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
await onClick(Click.Cancel);
|
await onClick(Click.Cancel);
|
||||||
|
|
||||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
const rendezvous = ref.current!.state.rendezvous!;
|
||||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user