Apply strictNullChecks to src/utils/*!exportUtils (#10455

* Apply `strictNullChecks` to `src/utils/exportUtils`

* strict fix

* fix strictNullChecks issues in some utils

* fix error message

* test coverage

* lint

* more strictNullChecks

* small optimisation for getUniqueRoomsWithIndividuals

* tidy

* test coverage
This commit is contained in:
Kerry 2023-04-03 20:26:55 +12:00 committed by GitHub
parent 4ed6e39067
commit 81a4498a8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 143 additions and 81 deletions

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from "react";
import {
AuthType,
IAuthData,
@ -23,8 +24,8 @@ import {
IStageStatus,
} from "matrix-js-sdk/src/interactive-auth";
import { MatrixClient } from "matrix-js-sdk/src/client";
import React, { createRef } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { UIAResponse } from "matrix-js-sdk/src/@types/uia";
import getEntryComponentForLoginType, { IStageComponent } from "../views/auth/InteractiveAuthEntryComponents";
import Spinner from "../views/elements/Spinner";
@ -39,7 +40,7 @@ type InteractiveAuthCallbackSuccess = (
type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void;
export type InteractiveAuthCallback = InteractiveAuthCallbackSuccess & InteractiveAuthCallbackFailure;
interface IProps {
export interface InteractiveAuthProps<T> {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
// response from initial request. If not supplied, will do a request on mount.
@ -61,7 +62,7 @@ interface IProps {
continueText?: string;
continueKind?: string;
// callback
makeRequest(auth: IAuthData | null): Promise<IAuthData>;
makeRequest(auth?: IAuthData): Promise<UIAResponse<T>>;
// callback called when the auth process has finished,
// successfully or unsuccessfully.
// @param {boolean} status True if the operation requiring
@ -92,14 +93,14 @@ interface IState {
submitButtonEnabled: boolean;
}
export default class InteractiveAuthComponent extends React.Component<IProps, IState> {
export default class InteractiveAuthComponent<T> extends React.Component<InteractiveAuthProps<T>, IState> {
private readonly authLogic: InteractiveAuth;
private readonly intervalId: number | null = null;
private readonly stageComponent = createRef<IStageComponent>();
private unmounted = false;
public constructor(props: IProps) {
public constructor(props: InteractiveAuthProps<T>) {
super(props);
this.state = {

View file

@ -22,7 +22,11 @@ import { AuthType, IAuthData } from "matrix-js-sdk/src/interactive-auth";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import InteractiveAuth, { ERROR_USER_CANCELLED, InteractiveAuthCallback } from "../../structures/InteractiveAuth";
import InteractiveAuth, {
ERROR_USER_CANCELLED,
InteractiveAuthCallback,
InteractiveAuthProps,
} from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import BaseDialog from "./BaseDialog";
@ -37,17 +41,11 @@ type DialogAesthetics = Partial<{
};
}>;
export interface InteractiveAuthDialogProps {
export interface InteractiveAuthDialogProps<T = unknown>
extends Pick<InteractiveAuthProps<T>, "makeRequest" | "authData"> {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
// response from initial request. If not supplied, will do a request on
// mount.
authData?: IAuthData;
// callback
makeRequest: (auth: IAuthData) => Promise<IAuthData>;
// Optional title and body to show when not showing a particular stage
title?: string;
body?: string;
@ -83,8 +81,8 @@ interface IState {
uiaStagePhase: number | null;
}
export default class InteractiveAuthDialog extends React.Component<InteractiveAuthDialogProps, IState> {
public constructor(props: InteractiveAuthDialogProps) {
export default class InteractiveAuthDialog<T> extends React.Component<InteractiveAuthDialogProps<T>, IState> {
public constructor(props: InteractiveAuthDialogProps<T>) {
super(props);
this.state = {

View file

@ -20,7 +20,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IClientWellKnown } from "matrix-js-sdk/src/matrix";
import { _t, UserFriendlyError } from "../languageHandler";
import { makeType } from "./TypeUtils";
import SdkConfig from "../SdkConfig";
import { ValidatedServerConfig } from "./ValidatedServerConfig";
@ -43,7 +42,7 @@ export default class AutoDiscoveryUtils {
* @param {string | Error} error The error to check
* @returns {boolean} True if the error is a liveliness error.
*/
public static isLivelinessError(error: string | Error): boolean {
public static isLivelinessError(error?: string | Error | null): boolean {
if (!error) return false;
return !!LIVELINESS_DISCOVERY_ERRORS.find((e) =>
typeof error === "string" ? e === error : e === error.message,
@ -197,7 +196,7 @@ export default class AutoDiscoveryUtils {
): ValidatedServerConfig {
if (!discoveryResult || !discoveryResult["m.homeserver"]) {
// This shouldn't happen without major misconfiguration, so we'll log a bit of information
// in the log so we can find this bit of codee but otherwise tell teh user "it broke".
// in the log so we can find this bit of code but otherwise tell the user "it broke".
logger.error("Ended up in a state of not knowing which homeserver to connect to.");
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
}
@ -216,7 +215,7 @@ export default class AutoDiscoveryUtils {
// of Element.
let preferredIdentityUrl = defaultConfig && defaultConfig["isUrl"];
if (isResult && isResult.state === AutoDiscovery.SUCCESS) {
preferredIdentityUrl = isResult["base_url"];
preferredIdentityUrl = isResult["base_url"] ?? undefined;
} else if (isResult && isResult.state !== AutoDiscovery.PROMPT) {
logger.error("Error determining preferred identity server URL:", isResult);
if (isResult.state === AutoDiscovery.FAIL_ERROR) {
@ -244,6 +243,12 @@ export default class AutoDiscoveryUtils {
}
const preferredHomeserverUrl = hsResult["base_url"];
if (!preferredHomeserverUrl) {
logger.error("No homeserver URL configured");
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
}
let preferredHomeserverName = serverName ? serverName : hsResult["server_name"];
const url = new URL(preferredHomeserverUrl);
@ -255,7 +260,7 @@ export default class AutoDiscoveryUtils {
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
}
return makeType(ValidatedServerConfig, {
return {
hsUrl: preferredHomeserverUrl,
hsName: preferredHomeserverName,
hsNameIsDifferent: url.hostname !== preferredHomeserverName,
@ -263,6 +268,6 @@ export default class AutoDiscoveryUtils {
isDefault: false,
warning: hsResult.error,
isNameResolvable: !isSynthetic,
});
} as ValidatedServerConfig;
}
}

View file

@ -103,8 +103,6 @@ export default class DMRoomMap {
}
private onAccountData = (ev: MatrixEvent): void => {
console.log("onAccountData");
if (ev.getType() == EventType.Direct) {
this.setMDirectFromContent(ev.getContent());
this.userToRooms = null;
@ -207,13 +205,16 @@ export default class DMRoomMap {
public getUniqueRoomsWithIndividuals(): { [userId: string]: Room } {
if (!this.roomToUser) return {}; // No rooms means no map.
return Object.keys(this.roomToUser)
.map((r) => ({ userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r) }))
.filter((r) => r.userId && r.room?.getInvitedAndJoinedMemberCount() === 2)
.reduce((obj, r) => {
obj[r.userId] = r.room;
return obj;
}, {} as Record<string, Room>);
// map roomToUser to valid rooms with two participants
return Object.keys(this.roomToUser).reduce((acc, roomId: string) => {
const userId = this.getUserIdForRoomId(roomId);
const room = this.matrixClient.getRoom(roomId);
const hasTwoMembers = room?.getInvitedAndJoinedMemberCount() === 2;
if (userId && room && hasTwoMembers) {
acc[userId] = room;
}
return acc;
}, {} as Record<string, Room>);
}
/**
@ -236,9 +237,7 @@ export default class DMRoomMap {
// to avoid multiple devices fighting to correct
// the account data, only try to send the corrected
// version once.
logger.warn(
`Invalid m.direct account data detected ` + `(self-chats that shouldn't be), patching it up.`,
);
logger.warn(`Invalid m.direct account data detected (self-chats that shouldn't be), patching it up.`);
if (neededPatching && !this.hasSentOutPatchDirectAccountDataPatch) {
this.hasSentOutPatchDirectAccountDataPatch = true;
this.matrixClient.setAccountData(EventType.Direct, userToRooms);

View file

@ -21,8 +21,8 @@ import SdkConfig from "../SdkConfig";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { Policies } from "../Terms";
export function getDefaultIdentityServerUrl(): string {
return SdkConfig.get("validated_server_config").isUrl;
export function getDefaultIdentityServerUrl(): string | undefined {
return SdkConfig.get("validated_server_config")?.isUrl;
}
export function setToDefaultIdentityServer(): void {

View file

@ -102,7 +102,10 @@ export class MediaEventHelper implements IDestroyable {
}
}
return fetch(this.media.thumbnailHttp).then((r) => r.blob());
const thumbnailHttp = this.media.thumbnailHttp;
if (!thumbnailHttp) return Promise.resolve(null);
return fetch(thumbnailHttp).then((r) => r.blob());
};
public static isEligible(event: MatrixEvent): boolean {

View file

@ -1,30 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Creates a class of a given type using the objects defined. This
* is a stopgap function while we don't have TypeScript interfaces.
* In future, we'd define the `type` as an interface and just cast
* it instead of cheating like we are here.
* @param {Type} Type The type of class to construct.
* @param {*} opts The options (properties) to set on the object.
* @returns {*} The created object.
*/
export function makeType<T>(Type: { new (): T }, opts: Partial<T>): T {
const c = new Type();
Object.assign(c, opts);
return c;
}

View file

@ -24,7 +24,7 @@ type FunctionWithUIA<R, A> = (auth?: IAuthData, ...args: A[]) => Promise<UIAResp
export function wrapRequestWithDialog<R, A = any>(
requestFunction: FunctionWithUIA<R, A>,
opts: Omit<InteractiveAuthDialogProps, "makeRequest" | "onFinished">,
opts: Omit<InteractiveAuthDialogProps<R>, "makeRequest" | "onFinished">,
): (...args: A[]) => Promise<R> {
return async function (...args): Promise<R> {
return new Promise((resolve, reject) => {

View file

@ -119,6 +119,7 @@ export default class WidgetUtils {
if (
testUrl.protocol === scalarUrl.protocol &&
testUrl.host === scalarUrl.host &&
scalarUrl.pathname &&
testUrl.pathname?.startsWith(scalarUrl.pathname)
) {
return true;

View file

@ -61,6 +61,12 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner =
}
const room = cli.getRoom(roomId);
// should not encounter this
if (!room) {
throw new Error(`Expected to find room for id ${roomId}`);
}
// await any queued messages being sent so that they do not fail
await Promise.all(
room

View file

@ -85,12 +85,14 @@ export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null =>
if (mLocation !== undefined) {
const uri = mLocation["uri"];
if (uri !== undefined) {
return makeMapSiteLink(parseGeoUri(uri));
const geoCoords = parseGeoUri(uri);
return geoCoords ? makeMapSiteLink(geoCoords) : null;
}
} else {
const geoUri = content["geo_uri"];
if (geoUri) {
return makeMapSiteLink(parseGeoUri(geoUri));
const geoCoords = parseGeoUri(geoUri);
return geoCoords ? makeMapSiteLink(geoCoords) : null;
}
}
return null;

View file

@ -28,16 +28,23 @@ export const parseGeoUri = (uri: string): GeolocationCoordinates | undefined =>
if (!m) return;
const parts = m[1].split(";");
const coords = parts[0].split(",");
let uncertainty: number | null;
let uncertainty: number | null | undefined = undefined;
for (const param of parts.slice(1)) {
const m = param.match(/u=(.*)/);
if (m) uncertainty = parse(m[1]);
}
const latitude = parse(coords[0]);
const longitude = parse(coords[1]);
if (latitude === null || longitude === null) {
return;
}
return {
latitude: parse(coords[0]),
longitude: parse(coords[1]),
latitude: latitude!,
longitude: longitude!,
altitude: parse(coords[2]),
accuracy: uncertainty,
accuracy: uncertainty!,
altitudeAccuracy: null,
heading: null,
speed: null,

View file

@ -120,7 +120,7 @@ export const reorderLexicographically = (
// verify the right move would be sufficient
if (
rightBoundIdx === newOrder.length - 1 &&
(newOrder[rightBoundIdx] ? stringToBase(newOrder[rightBoundIdx].order) : BigInt(Number.MAX_VALUE)) -
(newOrder[rightBoundIdx]?.order ? stringToBase(newOrder[rightBoundIdx].order!) : BigInt(Number.MAX_VALUE)) -
prevBase <=
rightBoundIdx - toIndex
) {

View file

@ -65,6 +65,9 @@ export const lookupThreePids = async (
if (threePids.length === 0) return [];
const token = await client.identityServer.getAccessToken();
if (!token) return [];
const lookedUp = await client.bulkLookupThreePids(
threePids.map((t) => [t.isEmail ? "email" : "msisdn", t.userId]),
token,

View file

@ -47,7 +47,6 @@ import { MapperOpts } from "matrix-js-sdk/src/event-mapper";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
import { makeType } from "../../src/utils/TypeUtils";
import { ValidatedServerConfig } from "../../src/utils/ValidatedServerConfig";
import { EnhancedMap } from "../../src/utils/maps";
import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
@ -591,13 +590,13 @@ export function mkStubRoom(
} as unknown as Room;
}
export function mkServerConfig(hsUrl: string, isUrl: string) {
return makeType(ValidatedServerConfig, {
export function mkServerConfig(hsUrl: string, isUrl: string): ValidatedServerConfig {
return {
hsUrl,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl,
});
} as ValidatedServerConfig;
}
// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent

View file

@ -16,7 +16,7 @@ limitations under the License.
import { mocked, Mocked } from "jest-mock";
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, EventType, IContent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { ClientEvent, EventType, IContent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import DMRoomMap from "../../src/utils/DMRoomMap";
import { mkEvent, stubClient } from "../test-utils";
@ -137,4 +137,56 @@ describe("DMRoomMap", () => {
expect(dmRoomMap.getRoomIds()).toEqual(new Set([roomId1, roomId2, roomId4]));
});
});
describe("getUniqueRoomsWithIndividuals()", () => {
const bigRoom = {
roomId: "!bigRoom:server.org",
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(5000),
} as unknown as Room;
const dmWithBob = {
roomId: "!dmWithBob:server.org",
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(2),
} as unknown as Room;
const dmWithCharlie = {
roomId: "!dmWithCharlie:server.org",
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(2),
} as unknown as Room;
const smallRoom = {
roomId: "!smallRoom:server.org",
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(3),
} as unknown as Room;
const mDirectContent = {
"@bob:server.org": [bigRoom.roomId, dmWithBob.roomId, smallRoom.roomId],
"@charlie:server.org": [dmWithCharlie.roomId, smallRoom.roomId],
};
beforeEach(() => {
client.getAccountData.mockReturnValue(mkMDirectEvent(mDirectContent));
client.getRoom.mockImplementation((roomId: string) =>
[bigRoom, smallRoom, dmWithCharlie, dmWithBob].find((room) => room.roomId === roomId),
);
});
it("returns an empty object when room map has not been populated", () => {
const instance = new DMRoomMap(client);
expect(instance.getUniqueRoomsWithIndividuals()).toEqual({});
});
it("returns map of users to rooms with 2 members", () => {
const dmRoomMap = new DMRoomMap(client);
dmRoomMap.start();
expect(dmRoomMap.getUniqueRoomsWithIndividuals()).toEqual({
"@bob:server.org": dmWithBob,
"@charlie:server.org": dmWithCharlie,
});
});
it("excludes rooms that are not found by matrixClient", () => {
client.getRoom.mockReset().mockReturnValue(undefined);
const dmRoomMap = new DMRoomMap(client);
dmRoomMap.start();
expect(dmRoomMap.getUniqueRoomsWithIndividuals()).toEqual({});
});
});
});

View file

@ -31,15 +31,23 @@ describe("createMapSiteLinkFromEvent", () => {
).toBeNull();
});
it("returns OpenStreetMap link if event contains m.location", () => {
it("returns OpenStreetMap link if event contains m.location with valid uri", () => {
expect(createMapSiteLinkFromEvent(makeLocationEvent("geo:51.5076,-0.1276"))).toEqual(
"https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + "#map=16/51.5076/-0.1276",
);
});
it("returns null if event contains m.location with invalid uri", () => {
expect(createMapSiteLinkFromEvent(makeLocationEvent("123 Sesame St"))).toBeNull();
});
it("returns OpenStreetMap link if event contains geo_uri", () => {
expect(createMapSiteLinkFromEvent(makeLegacyLocationEvent("geo:51.5076,-0.1276"))).toEqual(
"https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + "#map=16/51.5076/-0.1276",
);
});
it("returns null if event contains an invalid geo_uri", () => {
expect(createMapSiteLinkFromEvent(makeLegacyLocationEvent("123 Sesame St"))).toBeNull();
});
});

View file

@ -21,6 +21,14 @@ describe("parseGeoUri", () => {
expect(parseGeoUri("")).toBeFalsy();
});
it("returns undefined if latitude is not a number", () => {
expect(parseGeoUri("geo:ABCD,16.3695,183")).toBeUndefined();
});
it("returns undefined if longitude is not a number", () => {
expect(parseGeoUri("geo:48.2010,EFGH,183")).toBeUndefined();
});
// We use some examples from the spec, but don't check semantics
// like two textually-different URIs being equal, since we are
// just a humble parser.