[Labs] Sign in with QR on new EW using generated QR for MSC4108 v2024 (#33184)
* PoC Sign in with QR on new EW using generated QR for MSC4108 v2024 * Revert package.json changes * Prettier * Fix i18n * Tidy up * Remove unused state variable * Iterate tests * Partial revert * Iterate * Wire up qr_login route * Iterate UI * Fix React dev mode double rendering issue * Fix react key warning * Hide flow header on login * Re-roll qr code on channel expiry * Switch to AbortSignal * Improve auto-retry QR UX * Ensure we only show sign in with QR button if enabled * XXX: enable labs flag on Netlify builds * Tweak QR code sizing * Move qr login flow into a dialog to match designs * Fix null deviceId * Remove duplicate log * Iterate * Fix tests * Fix types * Fix tests * Fix tests * Make Netlify more useful * Make Netlify more useful v2 * Update copy * 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 * Iterate * Iterate * Refactor to handle most of the TODOs * Remove unused code * Remove unused code * Use js-sdk isSignInWithQRAvailable API to simplify code * Restore app-test.ts * Improve coverage * Improve coverage * Remove unused prop/state * Iterate * Fix tests * Iterate * Tests * Handle TODOs * Docs * Remove redundant call to crossSignDevice() * Workaround to remove training slash on the serverName before auto-discovery * Revert "Workaround to remove training slash on the serverName before auto-discovery" This reverts commit 0335a8fdd1b8e8d949ab7fca17c76f8fab335b58. * setLoggedIn not to be used with OIDC flows as it clears storage as per docs on setLoggedIn we should use restoreSessionFromStorage * Don't show the security_code_prompt unconditionally(i.e. for the web logging in mobile flow) * Update LoginWithQRFlow-test.tsx.snap * Update MatrixChat-test from setLoggedInSpy to restoreSessionSpy * Add todo for server switch * Add todo about handling base URL or server name * Handle server name or base URL being returned * Format * Fix loading state height * Handle the homeserver URL differing during QR code login * Comments * Comments * Register OIDC client ID after homeserver swap * Make QrLoginDialog async to minimise the impact on bundle size * Handle unsupported HS earlier in the flow * Iterate * Delint * Fix test * Discard changes to apps/web/element.io/develop/config.json --------- Co-authored-by: Hugh Nimmo-Smith <hughns@element.io> Co-authored-by: David Langley <langley.dave@gmail.com>
This commit is contained in:
co-authored by
Hugh Nimmo-Smith
David Langley
parent
d1a6137c90
commit
486fa57b68
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import React, { type ComponentProps } from "react";
|
||||
import React, { type ComponentProps, createRef, type RefObject } from "react";
|
||||
import { fireEvent, render, type RenderResult, screen, waitFor, within, act } from "jest-matrix-react";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { ClientEvent, type MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
UserVerificationStatus,
|
||||
type CryptoApi,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
import * as qrLogin from "matrix-js-sdk/src/rendezvous";
|
||||
|
||||
import MatrixChat from "../../../../src/components/structures/MatrixChat";
|
||||
import * as StorageAccess from "../../../../src/utils/StorageAccess";
|
||||
@@ -69,6 +71,8 @@ import { clearStorage } from "../../../../src/Lifecycle";
|
||||
import RoomListStore from "../../../../src/stores/room-list/RoomListStore.ts";
|
||||
import UserSettingsDialog from "../../../../src/components/views/dialogs/UserSettingsDialog.tsx";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext.ts";
|
||||
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc.ts";
|
||||
import { type QrLoginCredentials } from "../../../../src/components/views/auth/LoginWithQR.tsx";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||
completeAuthorizationCodeGrant: jest.fn(),
|
||||
@@ -82,6 +86,31 @@ jest.mock("../../../../src/theme");
|
||||
/** The matrix versions our mock server claims to support */
|
||||
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
|
||||
|
||||
function createMockCrypto(): CryptoApi {
|
||||
return {
|
||||
getVersion: jest.fn().mockReturnValue("Version 0"),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn().mockReturnValue({
|
||||
get: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
"devid",
|
||||
{ deviceId: "devid", dehydrated: false, getIdentityKey: jest.fn().mockReturnValue("k") },
|
||||
],
|
||||
]),
|
||||
),
|
||||
}),
|
||||
getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(true, true, false)),
|
||||
setDeviceIsolationMode: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockReturnValue(false),
|
||||
getDeviceVerificationStatus: jest.fn().mockResolvedValue({ signedByOwner: true } as DeviceVerificationStatus),
|
||||
isCrossSigningReady: jest.fn().mockReturnValue(false),
|
||||
requestOwnUserVerification: jest.fn().mockResolvedValue({ cancel: jest.fn(), on: jest.fn() }),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("<MatrixChat />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const deviceId = "qwertyui";
|
||||
@@ -179,8 +208,11 @@ describe("<MatrixChat />", () => {
|
||||
warning: "",
|
||||
};
|
||||
let defaultProps: ComponentProps<typeof MatrixChat>;
|
||||
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) => {
|
||||
return render(<MatrixChat {...defaultProps} {...props} />);
|
||||
const getComponent = (
|
||||
props: Partial<ComponentProps<typeof MatrixChat>> = {},
|
||||
ref?: RefObject<MatrixChat | null>,
|
||||
) => {
|
||||
return render(<MatrixChat {...defaultProps} {...props} ref={ref} />);
|
||||
};
|
||||
|
||||
// make test results readable
|
||||
@@ -311,6 +343,118 @@ describe("<MatrixChat />", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("qr login", () => {
|
||||
beforeEach(() => {
|
||||
const authConfig = makeDelegatedAuthConfig();
|
||||
defaultProps.config.validated_server_config!.delegatedAuthentication = authConfig;
|
||||
fetchMock.post(authConfig.registration_endpoint!, { client_id: "abc123" });
|
||||
mockPlatformPeg({
|
||||
getOidcClientMetadata: jest.fn().mockReturnValue({
|
||||
clientName: "App name",
|
||||
clientUri: "https://company",
|
||||
redirectUris: ["https://app"],
|
||||
logoUri: "https://company/logo.png",
|
||||
applicationType: "web",
|
||||
}),
|
||||
});
|
||||
jest.spyOn(qrLogin, "signInByGeneratingQR").mockReturnValue(new Promise(() => {}));
|
||||
});
|
||||
|
||||
it("should open QrLoginDialog on ViewQrLogin action", async () => {
|
||||
getComponent();
|
||||
defaultDispatcher.fire(Action.ViewQrLogin);
|
||||
await expect(screen.findByRole("dialog", { name: "Sign in with QR code" })).resolves.toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should ignore ViewQrLogin action when logged in", async () => {
|
||||
await populateStorageForSession();
|
||||
getComponent();
|
||||
// wait for logged in view to load
|
||||
await screen.findByLabelText("User menu");
|
||||
|
||||
defaultDispatcher.fire(Action.ViewQrLogin);
|
||||
expect(screen.queryByRole("dialog", { name: "Sign in with QR code" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should fire ViewQrLogin action on 'qr_login' route", async () => {
|
||||
const ref = createRef<MatrixChat>();
|
||||
getComponent({}, ref);
|
||||
ref.current!.showScreen("qr_login");
|
||||
await expect(screen.findByRole("dialog", { name: "Sign in with QR code" })).resolves.toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle qr login completed", async () => {
|
||||
const qrCreds: QrLoginCredentials = {
|
||||
accessToken: "at",
|
||||
homeserverUrl: "https://homeserver",
|
||||
clientId: "ci",
|
||||
idToken: "it",
|
||||
issuer: defaultProps.config.validated_server_config!.delegatedAuthentication!.issuer,
|
||||
deviceId: "di",
|
||||
secrets: {
|
||||
cross_signing: {
|
||||
master_key: "mk",
|
||||
self_signing_key: "ssk",
|
||||
user_signing_key: "usk",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockClient.whoami.mockResolvedValue({ user_id: "@user:homeserver", device_id: qrCreds.deviceId });
|
||||
mockClient.getCrypto.mockReturnValue({
|
||||
...createMockCrypto(),
|
||||
crossSignDevice: jest.fn().mockResolvedValue(undefined),
|
||||
importSecretsBundle: jest.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
getComponent();
|
||||
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({} as any);
|
||||
|
||||
// Assert welcome screen
|
||||
await screen.findByText("Welcome to Test");
|
||||
|
||||
// Open QR dialog so we can grab the onLoggedIn method
|
||||
defaultDispatcher.fire(Action.ViewQrLogin, true);
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{
|
||||
onLoggedIn: expect.any(Function),
|
||||
serverConfig: defaultProps.config.validated_server_config,
|
||||
},
|
||||
"mx_LoginWithQR_dialog",
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const { onLoggedIn } = createDialogSpy.mock.calls[0][1] as {
|
||||
onLoggedIn(creds: QrLoginCredentials): Promise<void>;
|
||||
};
|
||||
|
||||
const configureFromCompletedSpy = jest.spyOn(Lifecycle, "configureFromCompletedOAuthLogin");
|
||||
const restoreSessionSpy = jest.spyOn(Lifecycle, "restoreSessionFromStorage");
|
||||
const prom = onLoggedIn(qrCreds);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(configureFromCompletedSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accessToken: qrCreds.accessToken,
|
||||
homeserverUrl: qrCreds.homeserverUrl,
|
||||
}),
|
||||
),
|
||||
);
|
||||
await waitFor(() => expect(restoreSessionSpy).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(mockClient.getCrypto()!.importSecretsBundle).toHaveBeenCalledWith(qrCreds.secrets),
|
||||
);
|
||||
|
||||
await prom;
|
||||
|
||||
// initial sync
|
||||
mockClient.emit(ClientEvent.Sync, SyncState.Prepared, null);
|
||||
// wait for logged in view to load
|
||||
await screen.findByLabelText("User menu");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when query params have a OIDC params", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const homeserverUrl = "https://matrix.org";
|
||||
@@ -1186,37 +1330,6 @@ describe("<MatrixChat />", () => {
|
||||
await screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
function createMockCrypto(): CryptoApi {
|
||||
return {
|
||||
getVersion: jest.fn().mockReturnValue("Version 0"),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn().mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
"devid",
|
||||
{
|
||||
deviceId: "devid",
|
||||
dehydrated: false,
|
||||
getIdentityKey: jest.fn().mockReturnValue("k"),
|
||||
},
|
||||
],
|
||||
]),
|
||||
),
|
||||
}),
|
||||
getUserVerificationStatus: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new UserVerificationStatus(true, true, false)),
|
||||
setDeviceIsolationMode: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockReturnValue(false),
|
||||
getDeviceVerificationStatus: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ signedByOwner: true } as DeviceVerificationStatus),
|
||||
isCrossSigningReady: jest.fn().mockReturnValue(false),
|
||||
requestOwnUserVerification: jest.fn().mockResolvedValue({ cancel: jest.fn(), on: jest.fn() }),
|
||||
} as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe("showScreen", () => {
|
||||
|
||||
+108
@@ -269,6 +269,114 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> qr login should fire ViewQrLogin action on 'qr_login' route 1`] = `
|
||||
<div
|
||||
aria-label="Sign in with QR code"
|
||||
class=""
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
<div
|
||||
class="mx_LoginWithQR"
|
||||
data-testid="login-with-qr"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_main"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_spinner"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginWithQR_buttons"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> qr login should open QrLoginDialog on ViewQrLogin action 1`] = `
|
||||
<div
|
||||
aria-label="Sign in with QR code"
|
||||
class=""
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
<div
|
||||
class="mx_LoginWithQR"
|
||||
data-testid="login-with-qr"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_main"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_spinner"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginWithQR_buttons"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> should render spinner while app is loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -28,6 +28,7 @@ import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import { type FeatureSettingKey } from "../../../../../src/settings/Settings.tsx";
|
||||
import { mockOpenIdConfiguration } from "../../../../test-utils/oidc.ts";
|
||||
|
||||
mockPlatformPeg({
|
||||
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
|
||||
@@ -48,6 +49,7 @@ jest.mock("../../../../../src/settings/SettingsStore", () => ({
|
||||
shouldHaveWarning: jest.fn(),
|
||||
disabledMessage: jest.fn(),
|
||||
settingIsOveriddenAtConfigLevel: jest.fn(),
|
||||
doesSettingSupportLevel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<UserSettingsDialog />", () => {
|
||||
@@ -71,6 +73,8 @@ describe("<UserSettingsDialog />", () => {
|
||||
getIgnoredUsers: jest.fn().mockResolvedValue([]),
|
||||
getPushers: jest.fn().mockResolvedValue([]),
|
||||
getProfileInfo: jest.fn().mockResolvedValue({}),
|
||||
getMediaConfig: jest.fn(),
|
||||
getAuthMetadata: jest.fn().mockResolvedValue(mockOpenIdConfiguration()),
|
||||
});
|
||||
sdkContext = new SdkContextClass();
|
||||
sdkContext.client = mockClient;
|
||||
|
||||
@@ -14,11 +14,22 @@ import {
|
||||
MSC4108FailureReason,
|
||||
MSC4108SignInWithQR,
|
||||
RendezvousError,
|
||||
RendezvousIntent,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { HTTPError, type MatrixClient, MatrixHttpApi } from "matrix-js-sdk/src/matrix";
|
||||
import { mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
|
||||
import {
|
||||
AutoDiscovery,
|
||||
AutoDiscoveryAction,
|
||||
HTTPError,
|
||||
type MatrixClient,
|
||||
MatrixHttpApi,
|
||||
OAuthGrantType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import LoginWithQR, { LoginWithQRFailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
|
||||
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
|
||||
import { mockPlatformPeg } from "../../../../../test-utils";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/rendezvous/transports");
|
||||
jest.mock("matrix-js-sdk/src/rendezvous/channels");
|
||||
@@ -50,6 +61,7 @@ function makeClient() {
|
||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||
getCrypto: jest.fn().mockReturnValue({}),
|
||||
getDomain: jest.fn(),
|
||||
getAuthMetadata: jest.fn().mockReturnValue(mockOpenIdConfiguration()),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
cli.http = new MatrixHttpApi(cli, {
|
||||
@@ -80,209 +92,313 @@ describe("<LoginWithQR />", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client = makeClient();
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("MSC4108", () => {
|
||||
const getComponent = (props: {
|
||||
client: MatrixClient;
|
||||
onFinished?: () => void;
|
||||
ref?: RefObject<LoginWithQR | null>;
|
||||
}) => <LoginWithQR {...defaultProps} {...props} />;
|
||||
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||
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(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
describe("reciprocate", () => {
|
||||
const getComponent = (props: {
|
||||
client: MatrixClient;
|
||||
onFinished?: () => void;
|
||||
ref?: RefObject<LoginWithQR | null>;
|
||||
}) => (
|
||||
<LoginWithQR
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
intent={RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE}
|
||||
/>
|
||||
);
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "generateCode");
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols");
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel");
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, onFinished, ref }));
|
||||
|
||||
// back
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Back);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("should open a new channel if expires before qr scan", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, onFinished, ref }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||
|
||||
// Expire the channel
|
||||
rendezvous.onFailure!(ClientRendezvousFailureReason.Expired);
|
||||
await jest.runAllTimersAsync();
|
||||
await waitFor(() => expect(ref.current!.state.rendezvous).toBeDefined());
|
||||
expect(ref.current!.state.rendezvous).not.toBe(rendezvous);
|
||||
});
|
||||
|
||||
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("should show error if check code doesn't match", 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, "12");
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
failureReason: LoginWithQRFailureReason.CheckCodeMismatch,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("reciprocates login", async () => {
|
||||
const ref = createRef<LoginWithQR>();
|
||||
jest.spyOn(global.window, "open");
|
||||
|
||||
render(getComponent({ client, ref }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockResolvedValue({});
|
||||
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");
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.shareSecrets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles errors during protocol negotiation", async () => {
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, ref }));
|
||||
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({
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
onClick: expect.any(Function),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||
|
||||
// back (cancel)
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Cancel);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("should open a new channel if expires before qr scan", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, onFinished, ref }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
onClick: expect.any(Function),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||
|
||||
// Expire the channel
|
||||
rendezvous.onFailure!(ClientRendezvousFailureReason.Expired);
|
||||
await jest.runAllTimersAsync();
|
||||
await waitFor(() => expect(ref.current!.state.rendezvous).toBeDefined());
|
||||
expect(ref.current!.state.rendezvous).not.toBe(rendezvous);
|
||||
});
|
||||
|
||||
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("should show error if check code doesn't match", 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),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve, "12");
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
failureReason: LoginWithQRFailureReason.CheckCodeMismatch,
|
||||
onClick: expect.any(Function),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("reciprocates login", async () => {
|
||||
const ref = createRef<LoginWithQR>();
|
||||
jest.spyOn(global.window, "open");
|
||||
|
||||
render(getComponent({ client, ref }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockResolvedValue({});
|
||||
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),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.WaitingForDevice,
|
||||
onClick: expect.any(Function),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
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 () => {
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, ref }));
|
||||
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 = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
|
||||
});
|
||||
});
|
||||
|
||||
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),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
|
||||
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 () => {
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, ref }));
|
||||
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),
|
||||
intent: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE,
|
||||
}),
|
||||
);
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Cancel);
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||
});
|
||||
});
|
||||
|
||||
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),
|
||||
}),
|
||||
describe("login", () => {
|
||||
const getComponent = (props: {
|
||||
client: MatrixClient;
|
||||
onFinished?: () => void;
|
||||
onLoggedIn?: () => Promise<void>;
|
||||
ref?: RefObject<LoginWithQR | null>;
|
||||
}) => (
|
||||
<LoginWithQR
|
||||
onLoggedIn={jest.fn()}
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
intent={RendezvousIntent.LOGIN_ON_NEW_DEVICE}
|
||||
/>
|
||||
);
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockRejectedValue(
|
||||
new HTTPError("Internal Server Error", 500),
|
||||
);
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
test("should handle qr login", async () => {
|
||||
fetchMock.get("https://hs/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"],
|
||||
});
|
||||
const authMetadata = {
|
||||
...mockOpenIdConfiguration("https://auth.org/", [OAuthGrantType.DeviceAuthorization]),
|
||||
jwks_uri: undefined,
|
||||
};
|
||||
fetchMock.get("https://hs/_matrix/client/unstable/org.matrix.msc2965/auth_metadata", authMetadata);
|
||||
fetchMock.post(authMetadata.registration_endpoint!, {
|
||||
client_id: "!client_id!",
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Error,
|
||||
failureReason: ClientRendezvousFailureReason.Unknown,
|
||||
mockPlatformPeg({
|
||||
getOidcClientMetadata: jest.fn().mockReturnValue({
|
||||
clientName: "App name",
|
||||
clientUri: "https://company",
|
||||
redirectUris: ["https://app"],
|
||||
logoUri: "https://company/logo.png",
|
||||
applicationType: "web",
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles user cancelling during reciprocation", async () => {
|
||||
const ref = createRef<LoginWithQR>();
|
||||
render(getComponent({ client, ref }));
|
||||
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),
|
||||
}),
|
||||
);
|
||||
const ref = createRef<LoginWithQR>();
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Cancel);
|
||||
render(getComponent({ client, ref }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockResolvedValue({
|
||||
secrets: {
|
||||
cross_signing: {
|
||||
master_key: "mk",
|
||||
self_signing_key: "ssk",
|
||||
user_signing_key: "usk",
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({ serverName: "hs" });
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
|
||||
userCode: "123456",
|
||||
});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "completeLoginOnNewDevice").mockResolvedValue({
|
||||
access_token: "token",
|
||||
token_type: "Bearer",
|
||||
});
|
||||
jest.spyOn(AutoDiscovery, "findClientConfig").mockResolvedValue({
|
||||
"m.homeserver": { base_url: "https://hs", state: AutoDiscoveryAction.SUCCESS },
|
||||
"m.identity_server": { state: AutoDiscoveryAction.PROMPT },
|
||||
});
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE,
|
||||
}),
|
||||
);
|
||||
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.WaitingForDevice,
|
||||
onClick: expect.any(Function),
|
||||
intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE,
|
||||
userCode: "123456",
|
||||
}),
|
||||
);
|
||||
|
||||
const rendezvous = ref.current!.state.rendezvous!;
|
||||
expect(rendezvous.shareSecrets).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+90
-56
@@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { cleanup, fireEvent, render, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
|
||||
import { ClientRendezvousFailureReason, MSC4108FailureReason, RendezvousIntent } from "matrix-js-sdk/src/rendezvous";
|
||||
|
||||
import { mockQRCodeRender, resetQRCodeMock, waitForQRCodeRender } from "../../../../../test-utils/qrcode";
|
||||
import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow";
|
||||
import { LoginWithQRFailureReason, type FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
|
||||
import { type FailureReason, LoginWithQRFailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
|
||||
import { Click, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
|
||||
|
||||
describe("<LoginWithQRFlow />", () => {
|
||||
@@ -27,72 +27,106 @@ describe("<LoginWithQRFlow />", () => {
|
||||
onClick?: () => Promise<void>;
|
||||
failureReason?: FailureReason;
|
||||
code?: Uint8Array;
|
||||
intent: RendezvousIntent;
|
||||
}) => <LoginWithQRFlow {...defaultProps} {...props} />;
|
||||
|
||||
beforeEach(() => {});
|
||||
|
||||
afterEach(() => {
|
||||
resetQRCodeMock();
|
||||
onClick.mockReset();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders spinner while loading", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.Loading }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders spinner whilst QR generating", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.ShowingQR }));
|
||||
expect(screen.getAllByTestId("spinner")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders QR code", async () => {
|
||||
mockQRCodeRender();
|
||||
const { container } = render(
|
||||
getComponent({ phase: Phase.ShowingQR, code: new TextEncoder().encode("mock-code") }),
|
||||
);
|
||||
// QR code is rendered async so we wait for it:
|
||||
await waitForQRCodeRender();
|
||||
expect(screen.getAllByAltText("QR Code")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders spinner while signing in", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.WaitingForDevice }));
|
||||
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
fireEvent.click(screen.getByTestId("cancel-button"));
|
||||
expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined);
|
||||
});
|
||||
|
||||
it("renders spinner while verifying", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.Verifying }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders check code confirmation", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.OutOfBandConfirmation }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
for (const failureReason of [
|
||||
...Object.values(MSC4108FailureReason),
|
||||
...Object.values(LoginWithQRFailureReason),
|
||||
...Object.values(ClientRendezvousFailureReason),
|
||||
]) {
|
||||
it(`renders ${failureReason}`, async () => {
|
||||
describe.each([RendezvousIntent.LOGIN_ON_NEW_DEVICE, RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE])(
|
||||
"%s",
|
||||
(intent) => {
|
||||
it("renders spinner while loading", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.Error,
|
||||
failureReason,
|
||||
phase: Phase.Loading,
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
expect(screen.getAllByTestId("cancellation-message")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("renders spinner whilst QR generating", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.ShowingQR,
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
expect(screen.getByTestId("spinner")).toBeVisible();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders QR code", async () => {
|
||||
mockQRCodeRender();
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.ShowingQR,
|
||||
code: new TextEncoder().encode("mock-code"),
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
// QR code is rendered async so we wait for it:
|
||||
await waitForQRCodeRender();
|
||||
expect(screen.getAllByAltText("QR Code")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders spinner while signing in", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.WaitingForDevice,
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
fireEvent.click(screen.getByTestId("cancel-button"));
|
||||
expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined);
|
||||
});
|
||||
|
||||
it("renders spinner while verifying", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.Verifying,
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders check code confirmation", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
for (const failureReason of [
|
||||
...Object.values(MSC4108FailureReason),
|
||||
...Object.values(LoginWithQRFailureReason),
|
||||
...Object.values(ClientRendezvousFailureReason),
|
||||
]) {
|
||||
it(`renders ${failureReason}`, async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.Error,
|
||||
failureReason,
|
||||
intent,
|
||||
}),
|
||||
);
|
||||
expect(screen.getAllByTestId("cancellation-message")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+1718
-101
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user