Remove abandoned MSC3886, MSC3903, MSC3906 implementations (#28274)

* Remove abandoned MSC3886, MSC3903, MSC3906 implementations

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove stale snapshots

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-24 13:58:39 +01:00 committed by GitHub
parent 6d0d237c79
commit 5b5348ec1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 60 additions and 1373 deletions

View file

@ -24,10 +24,6 @@ export enum Phase {
WaitingForDevice,
Verifying,
Error,
/**
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
*/
LegacyConnected,
}
export enum Click {

View file

@ -9,11 +9,6 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import {
ClientRendezvousFailureReason,
LegacyRendezvousFailureReason,
MSC3886SimpleHttpRendezvousTransport,
MSC3903ECDHPayload,
MSC3903ECDHv2RendezvousChannel,
MSC3906Rendezvous,
MSC4108FailureReason,
MSC4108RendezvousSession,
MSC4108SecureChannel,
@ -23,29 +18,21 @@ import {
RendezvousIntent,
} from "matrix-js-sdk/src/rendezvous";
import { logger } from "matrix-js-sdk/src/logger";
import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Click, Mode, Phase } from "./LoginWithQR-types";
import LoginWithQRFlow from "./LoginWithQRFlow";
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
import { _t } from "../../../languageHandler";
interface IProps {
client: MatrixClient;
mode: Mode;
legacy: boolean;
onFinished(...args: any): void;
}
interface IState {
phase: Phase;
rendezvous?: MSC3906Rendezvous | MSC4108SignInWithQR;
rendezvous?: MSC4108SignInWithQR;
mediaPermissionError?: boolean;
// MSC3906
confirmationDigits?: string;
// MSC4108
verificationUri?: string;
userCode?: string;
checkCode?: string;
@ -54,25 +41,18 @@ interface IState {
}
export enum LoginWithQRFailureReason {
/**
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
*/
RateLimited = "rate_limited",
CheckCodeMismatch = "check_code_mismatch",
}
export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO: define an end-of-life date for this implementation.
/**
* A component that allows sign in and E2EE set up with a QR code.
*
* It implements `login.reciprocate` capabilities and showing QR codes.
*
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
* This uses the unstable feature of MSC4108: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
*/
export default class LoginWithQR extends React.Component<IProps, IState> {
private finished = false;
@ -104,9 +84,6 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
if (this.state.rendezvous) {
const rendezvous = this.state.rendezvous;
rendezvous.onFailure = undefined;
if (rendezvous instanceof MSC3906Rendezvous) {
await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled);
}
this.setState({ rendezvous: undefined });
}
if (mode === Mode.Show) {
@ -119,60 +96,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
// eslint-disable-next-line react/no-direct-mutation-state
this.state.rendezvous.onFailure = undefined;
// calling cancel will call close() as well to clean up the resources
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled);
} else {
this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled);
}
}
}
private async legacyApproveLogin(): Promise<void> {
if (!(this.state.rendezvous instanceof MSC3906Rendezvous)) {
throw new Error("Rendezvous not found");
}
if (!this.props.client) {
throw new Error("No client to approve login with");
}
this.setState({ phase: Phase.Loading });
try {
logger.info("Requesting login token");
const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, {
matrixClient: this.props.client,
title: _t("auth|qr_code_login|sign_in_new_device"),
})();
this.setState({ phase: Phase.WaitingForDevice });
const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken);
if (!newDeviceId) {
// user denied
return;
}
if (!this.props.client.getCrypto()) {
// no E2EE to set up
this.onFinished(true);
return;
}
this.setState({ phase: Phase.Verifying });
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
// clean up our state:
try {
await this.state.rendezvous.close();
} finally {
this.setState({ rendezvous: undefined });
}
this.onFinished(true);
} catch (e) {
logger.error("Error whilst approving sign in", e);
if (e instanceof HTTPError && e.httpStatus === 429) {
// 429: rate limit
this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited });
return;
}
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled);
}
}
@ -182,28 +106,18 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
}
private generateAndShowCode = async (): Promise<void> => {
let rendezvous: MSC4108SignInWithQR | MSC3906Rendezvous;
let rendezvous: MSC4108SignInWithQR;
try {
const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server;
if (this.props.legacy) {
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
onFailure: this.onFailure,
client: this.props.client,
fallbackRzServer,
});
const channel = new MSC3903ECDHv2RendezvousChannel(transport, undefined, this.onFailure);
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
} else {
const transport = new MSC4108RendezvousSession({
onFailure: this.onFailure,
client: this.props.client,
fallbackRzServer,
});
await transport.send("");
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);
rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure);
}
const transport = new MSC4108RendezvousSession({
onFailure: this.onFailure,
client: this.props.client,
fallbackRzServer,
});
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({
@ -218,10 +132,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
}
try {
if (rendezvous instanceof MSC3906Rendezvous) {
const confirmationDigits = await rendezvous.startAfterShowingCode();
this.setState({ phase: Phase.LegacyConnected, confirmationDigits });
} else if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) {
if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) {
// MSC4108-Flow: NewScanned
await rendezvous.negotiateProtocols();
const { verificationUri } = await rendezvous.deviceAuthorizationGrant();
@ -234,18 +145,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
// we ask the user to confirm that the channel is secure
} catch (e: RendezvousError | unknown) {
logger.error("Error whilst approving login", e);
if (rendezvous instanceof MSC3906Rendezvous) {
// only set to error phase if it hasn't already been set by onFailure or similar
if (this.state.phase !== Phase.Error) {
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown });
}
} else {
await rendezvous?.cancel(
e instanceof RendezvousError
? (e.code as MSC4108FailureReason)
: ClientRendezvousFailureReason.Unknown,
);
}
await rendezvous?.cancel(
e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown,
);
}
};
@ -298,7 +200,6 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
public reset(): void {
this.setState({
rendezvous: undefined,
confirmationDigits: undefined,
verificationUri: undefined,
failureReason: undefined,
userCode: undefined,
@ -311,16 +212,12 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
private onClick = async (type: Click, checkCode?: string): Promise<void> => {
switch (type) {
case Click.Cancel:
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
} else {
await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled);
}
await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled);
this.reset();
this.onFinished(false);
break;
case Click.Approve:
await (this.props.legacy ? this.legacyApproveLogin() : this.approveLogin(checkCode));
await this.approveLogin(checkCode);
break;
case Click.Decline:
await this.state.rendezvous?.declineLoginOnExistingDevice();
@ -328,11 +225,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
this.onFinished(false);
break;
case Click.Back:
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
} else {
await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled);
}
await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled);
this.onFinished(false);
break;
case Click.ShowQr:
@ -342,20 +235,6 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
};
public render(): React.ReactNode {
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
return (
<LoginWithQRFlow
onClick={this.onClick}
phase={this.state.phase}
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
confirmationDigits={
this.state.phase === Phase.LegacyConnected ? this.state.confirmationDigits : undefined
}
failureReason={this.state.failureReason}
/>
);
}
return (
<LoginWithQRFlow
onClick={this.onClick}

View file

@ -7,11 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, ReactNode } from "react";
import {
ClientRendezvousFailureReason,
LegacyRendezvousFailureReason,
MSC4108FailureReason,
} from "matrix-js-sdk/src/rendezvous";
import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-left";
import CheckCircleSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
@ -23,21 +19,11 @@ import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import QRCode from "../elements/QRCode";
import Spinner from "../elements/Spinner";
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
import { Click, Phase } from "./LoginWithQR-types";
import SdkConfig from "../../../SdkConfig";
import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR";
import { XOR } from "../../../@types/common";
import { ErrorMessage } from "../../structures/ErrorMessage";
/**
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
*/
interface MSC3906Props extends Pick<Props, "phase" | "onClick" | "failureReason"> {
code?: string;
confirmationDigits?: string;
}
interface Props {
phase: Phase;
code?: Uint8Array;
@ -47,19 +33,15 @@ interface Props {
checkCode?: string;
}
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO: define an end-of-life date for this implementation.
/**
* A component that implements the UI for sign in and E2EE set up with a QR code.
*
* This supports the unstable features of MSC3906 and MSC4108
* This supports the unstable features of MSC4108
*/
export default class LoginWithQRFlow extends React.Component<XOR<Props, MSC3906Props>> {
export default class LoginWithQRFlow extends React.Component<Props> {
private checkCodeInput = createRef<HTMLInputElement>();
public constructor(props: XOR<Props, MSC3906Props>) {
public constructor(props: Props) {
super(props);
}
@ -104,20 +86,17 @@ export default class LoginWithQRFlow extends React.Component<XOR<Props, MSC3906P
switch (this.props.failureReason) {
case MSC4108FailureReason.UnsupportedProtocol:
case LegacyRendezvousFailureReason.UnsupportedProtocol:
title = _t("auth|qr_code_login|error_unsupported_protocol_title");
message = _t("auth|qr_code_login|error_unsupported_protocol");
break;
case MSC4108FailureReason.UserCancelled:
case LegacyRendezvousFailureReason.UserCancelled:
title = _t("auth|qr_code_login|error_user_cancelled_title");
message = _t("auth|qr_code_login|error_user_cancelled");
break;
case MSC4108FailureReason.AuthorizationExpired:
case ClientRendezvousFailureReason.Expired:
case LegacyRendezvousFailureReason.Expired:
title = _t("auth|qr_code_login|error_expired_title");
message = _t("auth|qr_code_login|error_expired");
break;
@ -162,7 +141,6 @@ export default class LoginWithQRFlow extends React.Component<XOR<Props, MSC3906P
message = _t("auth|qr_code_login|error_etag_missing");
break;
case LegacyRendezvousFailureReason.HomeserverLacksSupport:
case ClientRendezvousFailureReason.HomeserverLacksSupport:
success = null;
Icon = QrCodeIcon;
@ -200,40 +178,6 @@ export default class LoginWithQRFlow extends React.Component<XOR<Props, MSC3906P
);
break;
}
case Phase.LegacyConnected:
backButton = false;
main = (
<>
<p>{_t("auth|qr_code_login|confirm_code_match")}</p>
<div className="mx_LoginWithQR_confirmationDigits">{this.props.confirmationDigits}</div>
<div className="mx_LoginWithQR_confirmationAlert">
<div>
<InfoIcon />
</div>
<div>{_t("auth|qr_code_login|approve_access_warning")}</div>
</div>
</>
);
buttons = (
<>
<AccessibleButton
data-testid="approve-login-button"
kind="primary"
onClick={this.handleClick(Click.Approve)}
>
{_t("action|approve")}
</AccessibleButton>
<AccessibleButton
data-testid="decline-login-button"
kind="primary_outline"
onClick={this.handleClick(Click.Decline)}
>
{_t("action|cancel")}
</AccessibleButton>
</>
);
break;
case Phase.OutOfBandConfirmation:
backButton = false;
main = (
@ -288,8 +232,7 @@ export default class LoginWithQRFlow extends React.Component<XOR<Props, MSC3906P
break;
case Phase.ShowingQR:
if (this.props.code) {
const data =
typeof this.props.code !== "string" ? this.props.code : Buffer.from(this.props.code ?? "");
const data = this.props.code;
main = (
<>

View file

@ -8,10 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import {
IGetLoginTokenCapability,
IServerVersions,
GET_LOGIN_TOKEN_CAPABILITY,
Capabilities,
IClientWellKnown,
OidcClientConfig,
MatrixClient,
@ -28,27 +25,11 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext
interface IProps {
onShowQr: () => void;
versions?: IServerVersions;
capabilities?: Capabilities;
wellKnown?: IClientWellKnown;
oidcClientConfig?: OidcClientConfig;
isCrossSigningReady?: boolean;
}
function shouldShowQrLegacy(
versions?: IServerVersions,
wellKnown?: IClientWellKnown,
capabilities?: Capabilities,
): boolean {
// Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886:
// in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability
const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
const getLoginTokenSupported =
!!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled;
const msc3886Supported =
!!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server;
return getLoginTokenSupported && msc3886Supported;
}
export function shouldShowQr(
cli: MatrixClient,
isCrossSigningReady: boolean,
@ -73,15 +54,12 @@ export function shouldShowQr(
const LoginWithQRSection: React.FC<IProps> = ({
onShowQr,
versions,
capabilities,
wellKnown,
oidcClientConfig,
isCrossSigningReady,
}) => {
const cli = useMatrixClientContext();
const offerShowQr = oidcClientConfig
? shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown)
: shouldShowQrLegacy(versions, wellKnown, capabilities);
const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown);
return (
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>

View file

@ -181,7 +181,6 @@ const SessionManagerTab: React.FC<{
const userId = matrixClient?.getUserId();
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]);
const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]);
const oidcClientConfig = useAsyncMemo(async () => {
try {
@ -292,12 +291,7 @@ const SessionManagerTab: React.FC<{
if (signInWithQrMode) {
return (
<Suspense fallback={<Spinner />}>
<LoginWithQR
mode={signInWithQrMode}
onFinished={onQrFinish}
client={matrixClient}
legacy={!oidcClientConfig && !showMsc4108QrCode}
/>
<LoginWithQR mode={signInWithQrMode} onFinished={onQrFinish} client={matrixClient} />
</Suspense>
);
}
@ -308,7 +302,6 @@ const SessionManagerTab: React.FC<{
<LoginWithQRSection
onShowQr={onShowQrClicked}
versions={clientVersions}
capabilities={capabilities}
wellKnown={wellKnown}
oidcClientConfig={oidcClientConfig}
isCrossSigningReady={isCrossSigningReady}

View file

@ -250,13 +250,11 @@
"phone_label": "Phone",
"phone_optional_label": "Phone (optional)",
"qr_code_login": {
"approve_access_warning": "By approving access for this device, it will have full access to your account.",
"check_code_explainer": "This will verify that the connection to your other device is secure.",
"check_code_heading": "Enter the number shown on your other device",
"check_code_input_label": "2-digit code",
"check_code_mismatch": "The numbers don't match",
"completing_setup": "Completing set up of your new device",
"confirm_code_match": "Check that the code below matches with your other device:",
"error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.",
"error_expired": "Sign in expired. Please try again.",
"error_expired_title": "The sign in was not completed in time",
@ -284,7 +282,6 @@
"security_code": "Security code",
"security_code_prompt": "If asked, enter the code below on your other device.",
"select_qr_code": "Select \"%(scanQRCode)s\"",
"sign_in_new_device": "Sign in new device",
"unsupported_explainer": "Your account provider doesn't support signing into a new device with a QR code.",
"unsupported_heading": "QR code not supported",
"waiting_for_device": "Waiting for device to sign in"

View file

@ -1,47 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { AuthDict } from "matrix-js-sdk/src/interactive-auth";
import { UIAResponse } from "matrix-js-sdk/src/matrix";
import Modal from "../Modal";
import InteractiveAuthDialog, { InteractiveAuthDialogProps } from "../components/views/dialogs/InteractiveAuthDialog";
type FunctionWithUIA<R, A> = (auth?: AuthDict, ...args: A[]) => Promise<UIAResponse<R>>;
export function wrapRequestWithDialog<R, A = any>(
requestFunction: FunctionWithUIA<R, A>,
opts: Omit<InteractiveAuthDialogProps<R>, "makeRequest" | "onFinished">,
): (...args: A[]) => Promise<R> {
return async function (...args): Promise<R> {
return new Promise((resolve, reject) => {
const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA<R, A>;
boundFunction(undefined, ...args)
.then((res) => resolve(res as R))
.catch((error) => {
if (error.httpStatus !== 401 || !error.data?.flows) {
// doesn't look like an interactive-auth failure
return reject(error);
}
Modal.createDialog(InteractiveAuthDialog, {
...opts,
authData: error.data,
makeRequest: (authData: AuthDict) => boundFunction(authData, ...args),
onFinished: (success, result) => {
if (success) {
resolve(result as R);
} else {
reject(result);
}
},
});
});
});
};
}

View file

@ -7,20 +7,18 @@ Please see LICENSE files in the repository root for full details.
*/
import { cleanup, render, waitFor } from "jest-matrix-react";
import { MockedObject, mocked } from "jest-mock";
import { mocked, MockedObject } from "jest-mock";
import React from "react";
import {
MSC3906Rendezvous,
LegacyRendezvousFailureReason,
ClientRendezvousFailureReason,
MSC4108SignInWithQR,
MSC4108FailureReason,
MSC4108SignInWithQR,
RendezvousError,
} from "matrix-js-sdk/src/rendezvous";
import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix";
import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
import LoginWithQR from "../../../../../../src/components/views/auth/LoginWithQR";
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
jest.mock("matrix-js-sdk/src/rendezvous");
jest.mock("matrix-js-sdk/src/rendezvous/transports");
@ -65,9 +63,6 @@ describe("<LoginWithQR />", () => {
mode: Mode.Show,
onFinished: jest.fn(),
};
const mockConfirmationDigits = "mock-confirmation-digits";
const mockRendezvousCode = "mock-rendezvous-code";
const newDeviceId = "new-device-id";
beforeEach(() => {
mockedFlow.mockReset();
@ -82,264 +77,10 @@ describe("<LoginWithQR />", () => {
cleanup();
});
describe("MSC3906", () => {
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
<React.StrictMode>
<LoginWithQR {...defaultProps} {...props} />
</React.StrictMode>
);
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
});
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),
}),
);
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).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} />
<LoginWithQR {...defaultProps} {...props} />
</React.StrictMode>
);
@ -363,7 +104,7 @@ describe("<LoginWithQR />", () => {
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Back);
expect(onFinished).toHaveBeenCalledWith(false);
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
});
test("failed to connect", async () => {
@ -404,6 +145,27 @@ describe("<LoginWithQR />", () => {
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
});
test("handles errors during protocol negotiation", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol);
// @ts-ignore work-around for lazy mocks
err.code = MSC4108FailureReason.UnsupportedProtocol;
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockRejectedValue(err);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.ShowingQR,
}),
),
);
await waitFor(() => {
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
});
});
test("handles errors during reciprocation", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});

View file

@ -8,11 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { cleanup, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import React from "react";
import {
ClientRendezvousFailureReason,
LegacyRendezvousFailureReason,
MSC4108FailureReason,
} from "matrix-js-sdk/src/rendezvous";
import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow";
import { LoginWithQRFailureReason, FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
@ -29,8 +25,7 @@ describe("<LoginWithQRFlow />", () => {
phase: Phase;
onClick?: () => Promise<void>;
failureReason?: FailureReason;
code?: string;
confirmationDigits?: string;
code?: Uint8Array;
}) => <LoginWithQRFlow {...defaultProps} {...props} />;
beforeEach(() => {});
@ -54,24 +49,14 @@ describe("<LoginWithQRFlow />", () => {
});
it("renders QR code", async () => {
const { container } = render(getComponent({ phase: Phase.ShowingQR, code: "mock-code" }));
const { container } = render(
getComponent({ phase: Phase.ShowingQR, code: new TextEncoder().encode("mock-code") }),
);
// QR code is rendered async so we wait for it:
await waitFor(() => screen.getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
it("renders code when connected", async () => {
const { container } = render(getComponent({ phase: Phase.LegacyConnected, confirmationDigits: "mock-digits" }));
expect(screen.getAllByText("mock-digits")).toHaveLength(1);
expect(screen.getAllByTestId("decline-login-button")).toHaveLength(1);
expect(screen.getAllByTestId("approve-login-button")).toHaveLength(1);
expect(container).toMatchSnapshot();
fireEvent.click(screen.getByTestId("decline-login-button"));
expect(onClick).toHaveBeenCalledWith(Click.Decline, undefined);
fireEvent.click(screen.getByTestId("approve-login-button"));
expect(onClick).toHaveBeenCalledWith(Click.Approve, undefined);
});
it("renders spinner while signing in", async () => {
const { container } = render(getComponent({ phase: Phase.WaitingForDevice }));
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
@ -92,7 +77,6 @@ describe("<LoginWithQRFlow />", () => {
describe("errors", () => {
for (const failureReason of [
...Object.values(LegacyRendezvousFailureReason),
...Object.values(MSC4108FailureReason),
...Object.values(LoginWithQRFailureReason),
...Object.values(ClientRendezvousFailureReason),

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { IClientWellKnown, IServerVersions, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "matrix-js-sdk/src/matrix";
import { IClientWellKnown, IServerVersions, MatrixClient } from "matrix-js-sdk/src/matrix";
import React from "react";
import fetchMock from "fetch-mock-jest";
@ -51,73 +51,6 @@ describe("<LoginWithQRSection />", () => {
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient({}));
});
describe("MSC3906", () => {
const defaultProps = {
onShowQr: () => {},
versions: makeVersions({}),
wellKnown: {},
};
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;
describe("should not render", () => {
it("no support at all", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("only get_login_token enabled", async () => {
const { container } = render(
getComponent({ capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } } }),
);
expect(container).toMatchSnapshot();
});
it("MSC3886 + get_login_token disabled", async () => {
const { container } = render(
getComponent({
versions: makeVersions({ "org.matrix.msc3886": true }),
capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: false } },
}),
);
expect(container).toMatchSnapshot();
});
});
describe("should render panel", () => {
it("get_login_token + MSC3886", async () => {
const { container } = render(
getComponent({
versions: makeVersions({
"org.matrix.msc3886": true,
}),
capabilities: {
[GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true },
},
}),
);
expect(container).toMatchSnapshot();
});
it("get_login_token + .well-known", async () => {
const wellKnown = {
"io.element.rendezvous": {
server: "https://rz.local",
},
};
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient(wellKnown));
const { container } = render(
getComponent({
versions: makeVersions({}),
capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } },
wellKnown,
}),
);
expect(container).toMatchSnapshot();
});
});
});
describe("MSC4108", () => {
describe("MSC4108", () => {
const defaultProps = {

View file

@ -252,48 +252,6 @@ exports[`<LoginWithQRFlow /> errors renders expired 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders expired 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
The sign in was not completed in time
</h1>
<p
data-testid="cancellation-message"
>
Sign in expired. Please try again.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
<div>
<div
@ -374,86 +332,6 @@ exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_heading"
>
<div
aria-label="Back"
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
data-testid="back-button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</div>
<div
class="mx_LoginWithQR_breadcrumbs"
>
Sessions
/
Link new device
</div>
</div>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
QR code not supported
</h1>
<p
data-testid="cancellation-message"
>
Your account provider doesn't support signing into a new device with a QR code.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders insecure_channel_detected 1`] = `
<div>
<div
@ -761,90 +639,6 @@ exports[`<LoginWithQRFlow /> errors renders unknown 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unknown 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p
data-testid="cancellation-message"
>
An unexpected error occurred. The request to connect your other device has been cancelled.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unsupported_algorithm 1`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p
data-testid="cancellation-message"
>
An unexpected error occurred. The request to connect your other device has been cancelled.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unsupported_protocol 1`] = `
<div>
<div
@ -887,48 +681,6 @@ exports[`<LoginWithQRFlow /> errors renders unsupported_protocol 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unsupported_protocol 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Other device not compatible
</h1>
<p
data-testid="cancellation-message"
>
This device does not support signing in to the other device with a QR code.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
<div>
<div
@ -971,48 +723,6 @@ exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_cancelled 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Sign in request cancelled
</h1>
<p
data-testid="cancellation-message"
>
The sign in was cancelled on the other device.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = `
<div>
<div
@ -1055,48 +765,6 @@ exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_declined 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Sign in declined
</h1>
<p
data-testid="cancellation-message"
>
You or the account provider declined the sign in request.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> renders QR code 1`] = `
<div>
<div
@ -1256,58 +924,6 @@ exports[`<LoginWithQRFlow /> renders check code confirmation 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> renders code when connected 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<p>
Check that the code below matches with your other device:
</p>
<div
class="mx_LoginWithQR_confirmationDigits"
>
mock-digits
</div>
<div
class="mx_LoginWithQR_confirmationAlert"
>
<div>
<div />
</div>
<div>
By approving access for this device, it will have full access to your account.
</div>
</div>
</div>
<div
class="mx_LoginWithQR_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="approve-login-button"
role="button"
tabindex="0"
>
Approve
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
data-testid="decline-login-button"
role="button"
tabindex="0"
>
Cancel
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> renders spinner while loading 1`] = `
<div>
<div

View file

@ -1,307 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LoginWithQRSection /> MSC3906 should not render MSC3886 + get_login_token disabled 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
<p
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
>
Not supported by your account provider
</p>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should not render no support at all 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
<p
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
>
Not supported by your account provider
</p>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should not render only get_login_token enabled 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
<p
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
>
Not supported by your account provider
</p>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should render panel get_login_token + .well-known 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should render panel get_login_token + MSC3886 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -1650,46 +1650,6 @@ describe("<SessionManagerTab />", () => {
expect(checkbox.getAttribute("aria-checked")).toEqual("false");
});
describe("MSC3906 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(false);
// enable server support for qr login
mockClient.getVersions.mockResolvedValue({
versions: [],
unstable_features: {
"org.matrix.msc3886": true,
},
});
mockClient.getCapabilities.mockResolvedValue({
[GET_LOGIN_TOKEN_CAPABILITY.name]: {
enabled: true,
},
});
});
it("renders qr code login section", async () => {
const { getByText } = render(getComponent());
// wait for versions call to settle
await flushPromises();
expect(getByText("Link new device")).toBeTruthy();
expect(getByText("Show QR code")).toBeTruthy();
});
it("enters qr code login section when show QR code button clicked", async () => {
const { getByText, findByTestId } = render(getComponent());
// wait for versions call to settle
await flushPromises();
fireEvent.click(getByText("Show QR code"));
await expect(findByTestId("login-with-qr")).resolves.toBeTruthy();
});
});
describe("MSC4108 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
const issuer = "https://issuer.org";