Merge remote-tracking branch 'origin/develop' into dbkr/refactor_createcrosssigningdialog

This commit is contained in:
David Baker
2024-10-18 14:40:46 +01:00
48 changed files with 468 additions and 534 deletions
@@ -0,0 +1,81 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import { act } from "@testing-library/react-hooks/dom";
import NewRecoveryMethodDialog from "../../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog";
import { createTestClient } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext.tsx";
import dis from "../../../../../src/dispatcher/dispatcher.ts";
import { Action } from "../../../../../src/dispatcher/actions.ts";
import Modal from "../../../../../src/Modal.tsx";
describe("<NewRecoveryMethodDialog />", () => {
let matrixClient: MatrixClient;
beforeEach(() => {
matrixClient = createTestClient();
jest.spyOn(dis, "fire");
jest.spyOn(Modal, "createDialog");
});
afterEach(() => {
jest.restoreAllMocks();
});
function renderComponent(onFinished: () => void = jest.fn()) {
return render(
<MatrixClientContext.Provider value={matrixClient}>
<NewRecoveryMethodDialog onFinished={onFinished} />
</MatrixClientContext.Provider>,
);
}
test("when cancel is clicked", async () => {
const onFinished = jest.fn();
act(() => {
renderComponent(onFinished);
});
await userEvent.click(screen.getByRole("button", { name: "Go to Settings" }));
expect(onFinished).toHaveBeenCalled();
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
});
test("when key backup is enabled", async () => {
jest.spyOn(matrixClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("version");
const onFinished = jest.fn();
await act(async () => {
const { asFragment } = renderComponent(onFinished);
await waitFor(() =>
expect(
screen.getByText("This session is encrypting history using the new recovery method."),
).toBeInTheDocument(),
);
expect(asFragment()).toMatchSnapshot();
});
await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
expect(onFinished).toHaveBeenCalled();
});
test("when key backup is disabled", async () => {
const onFinished = jest.fn();
const { asFragment } = renderComponent(onFinished);
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
await waitFor(() => expect(Modal.createDialog).toHaveBeenCalled());
});
});
@@ -0,0 +1,146 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NewRecoveryMethodDialog /> when key backup is disabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<span
class="mx_KeyBackupFailedDialog_title"
>
New Recovery Method
</span>
</h1>
</div>
<p>
A new Security Phrase and key for Secure Messages have been detected.
</p>
<strong
class="warning"
>
If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.
</strong>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Go to Settings
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Set up Secure Messages
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<NewRecoveryMethodDialog /> when key backup is enabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<span
class="mx_KeyBackupFailedDialog_title"
>
New Recovery Method
</span>
</h1>
</div>
<p>
A new Security Phrase and key for Secure Messages have been detected.
</p>
<p>
This session is encrypting history using the new recovery method.
</p>
<strong
class="warning"
>
If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.
</strong>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Go to Settings
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Set up Secure Messages
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
@@ -22,7 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import MatrixChat from "../../../../src/components/structures/MatrixChat";
import * as StorageAccess from "../../../../src/utils/StorageAccess";
@@ -137,6 +137,7 @@ describe("<MatrixChat />", () => {
userHasCrossSigningKeys: jest.fn(),
// This needs to not finish immediately because we need to test the screen appears
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
}),
secretStorage: {
isStored: jest.fn().mockReturnValue(null),
@@ -1515,4 +1516,22 @@ describe("<MatrixChat />", () => {
expect(screen.getByTestId("mobile-register")).toBeInTheDocument();
});
});
describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => {
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
jest.spyOn(mockClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("version");
getComponent({});
defaultDispatcher.dispatch({
action: "will_start_client",
});
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(screen.getByText("mocked dialog")).toBeInTheDocument());
});
});
});
@@ -10,7 +10,10 @@ import React from "react";
import fetchMock from "fetch-mock-jest";
import { render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
import ChangelogDialog from "../../../../../src/components/views/dialogs/ChangelogDialog";
import ChangelogDialog, {
DevelopVersionString,
parseVersion,
} from "../../../../../src/components/views/dialogs/ChangelogDialog";
describe("<ChangelogDialog />", () => {
it("should fetch github proxy url for each repo with old and new version strings", async () => {
@@ -64,8 +67,8 @@ describe("<ChangelogDialog />", () => {
files: [],
});
const newVersion = "newsha1-js-newsha3";
const oldVersion = "oldsha1-js-oldsha3";
const newVersion = "newsha1-js-newsha3" as DevelopVersionString;
const oldVersion = "oldsha1-js-oldsha3" as DevelopVersionString;
const { asFragment } = render(
<ChangelogDialog newVersion={newVersion} version={oldVersion} onFinished={jest.fn()} />,
);
@@ -78,3 +81,24 @@ describe("<ChangelogDialog />", () => {
expect(asFragment()).toMatchSnapshot();
});
});
describe("parseVersion", () => {
it("should return null for old-style version strings", () => {
expect(parseVersion("aaaabbbb-react-ccccdddd-js-eeeeffff")).toBeNull();
});
it("should return null for invalid version strings", () => {
expect(parseVersion("aaaabbbb-react-ccccdddd")).toBeNull();
});
it("should return null for release version strings", () => {
expect(parseVersion("v1.22.33")).toBeNull();
});
it("should return mapping for develop version string", () => {
expect(parseVersion("aaaabbbb-js-eeeeffff")).toEqual({
"element-hq/element-web": "aaaabbbb",
"matrix-org/matrix-js-sdk": "eeeeffff",
});
});
});
@@ -7,19 +7,24 @@ Please see LICENSE files in the repository root for full details.
*/
import {
EventTimeline,
EventTimelineSet,
EventType,
IRoomEvent,
MatrixClient,
MatrixEvent,
MsgType,
Relations,
RelationType,
Room,
RoomMember,
RoomState,
} from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest";
import escapeHtml from "escape-html";
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../../test-utils";
import { filterConsole, mkReaction, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../../test-utils";
import { ExportType, IExportOptions } from "../../../../src/utils/exportUtils/exportUtils";
import SdkConfig from "../../../../src/SdkConfig";
import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport";
@@ -123,6 +128,35 @@ describe("HTMLExport", () => {
fetchMock.get(media.srcHttp!, body);
}
function mockReactionForMessage(message: IRoomEvent): MatrixEvent {
const firstMessage = new MatrixEvent(message);
const reaction = mkReaction(firstMessage);
const relationsContainer = {
getRelations: jest.fn(),
getChildEventsForEvent: jest.fn(),
} as unknown as RelationsContainer;
const relations = new Relations(RelationType.Annotation, EventType.Reaction, client);
relations.addEvent(reaction);
relationsContainer.getChildEventsForEvent = jest
.fn()
.mockImplementation(
(eventId: string, relationType: RelationType | string, eventType: EventType | string) => {
if (eventId === firstMessage.getId()) {
return relations;
}
},
);
const timelineSet = {
relations: relationsContainer,
getLiveTimeline: () => timeline,
} as unknown as EventTimelineSet;
const timeline = new EventTimeline(timelineSet);
room.getUnfilteredTimelineSet = jest.fn().mockReturnValue(timelineSet);
return reaction;
}
it("should throw when created with invalid config for LastNMessages", async () => {
expect(
() =>
@@ -167,6 +201,7 @@ describe("HTMLExport", () => {
body: `Message #${i}`,
},
}));
mockReactionForMessage(events[0]);
mockMessages(...events);
const exporter = new HTMLExporter(
@@ -587,4 +622,24 @@ describe("HTMLExport", () => {
expect(await file.text()).toContain("testing testing");
expect(client.createMessagesRequest).not.toHaveBeenCalled();
});
it("should include reactions", async () => {
const reaction = mockReactionForMessage(EVENT_MESSAGE);
mockMessages(EVENT_MESSAGE);
const exporter = new HTMLExporter(
room,
ExportType.LastNMessages,
{
attachmentsIncluded: false,
maxSize: 1_024 * 1_024,
numberOfMessages: 40,
},
() => {},
);
await exporter.export();
const file = getMessageFile(exporter);
expect(await file.text()).toContain(reaction.getContent()["m.relates_to"]?.key);
});
});
File diff suppressed because one or more lines are too long