mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
chore: Reduce no-explicit-any warnings in server directory (#12202)
* chore: Reduce no-explicit-any warnings in server directory Tightens types across test response bodies, decorator signatures, the TestServer wrapper, base class generics, and presenter Record types. Where any is genuinely load-bearing (Sequelize model generics, PropertyDescriptor decorator returns, plugin-registered template classes, Fix mixin), keeps any with a targeted eslint-disable plus reason rather than masking the constraint. Cuts server-only no-explicit-any warnings from 162 to 70. * fix: groups test asserts on first response instead of second Caught by Copilot review on the no-explicit-any cleanup. Also fixes the pre-existing getChangsetSkipped → getChangesetSkipped typo surfaced while reviewing nearby decorator code.
This commit is contained in:
@@ -2,6 +2,7 @@ import { Hook, PluginManager } from "@server/utils/PluginManager";
|
||||
import { requireDirectory } from "@server/utils/fs";
|
||||
import type BaseEmail from "./BaseEmail";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- registry of heterogeneous template Props subtypes; BaseEmail<EmailProps> isn't assignable from BaseEmail<Subtype>.
|
||||
const emails: Record<string, typeof BaseEmail<any>> = {};
|
||||
|
||||
requireDirectory<{ default: typeof BaseEmail }>(__dirname).forEach(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { buildTeam } from "@server/test/factories";
|
||||
import { AuthenticationProvider } from "@server/models";
|
||||
import { AuthenticationProvider, type User } from "@server/models";
|
||||
import { createContext } from "@server/context";
|
||||
|
||||
describe("AuthenticationProvider", () => {
|
||||
@@ -15,7 +15,7 @@ describe("AuthenticationProvider", () => {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const ctx = createContext({ user: { team } as any });
|
||||
const ctx = createContext({ user: { team } as unknown as User });
|
||||
await expect(provider.disable(ctx)).resolves.not.toThrow();
|
||||
expect(provider.enabled).toBe(false);
|
||||
});
|
||||
@@ -37,7 +37,7 @@ describe("AuthenticationProvider", () => {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const ctx = createContext({ user: { team } as any });
|
||||
const ctx = createContext({ user: { team } as unknown as User });
|
||||
await expect(provider1.disable(ctx)).resolves.not.toThrow();
|
||||
expect(provider1.enabled).toBe(false);
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe("AuthenticationProvider", () => {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const ctx = createContext({ user: { team } as any });
|
||||
const ctx = createContext({ user: { team } as unknown as User });
|
||||
await expect(provider.disable(ctx)).rejects.toThrow(
|
||||
"At least one authentication provider is required"
|
||||
);
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import type {
|
||||
CollectionSort,
|
||||
ImportableIntegrationService,
|
||||
ProsemirrorData,
|
||||
SourceMetadata,
|
||||
NavigationNode,
|
||||
@@ -510,7 +511,7 @@ class Collection extends ParanoidModel<
|
||||
importId: string | null;
|
||||
|
||||
@BelongsTo(() => Import, "apiImportId")
|
||||
apiImport: Import<any> | null;
|
||||
apiImport: Import<ImportableIntegrationService> | null;
|
||||
|
||||
@ForeignKey(() => Import)
|
||||
@Column(DataType.UUID)
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { MaxLength } from "class-validator";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import type {
|
||||
ImportableIntegrationService,
|
||||
NavigationNode,
|
||||
ProsemirrorData,
|
||||
SourceMetadata,
|
||||
@@ -609,7 +610,7 @@ class Document extends ArchivableModel<
|
||||
importId: string | null;
|
||||
|
||||
@BelongsTo(() => Import, "apiImportId")
|
||||
apiImport: Import<any> | null;
|
||||
apiImport: Import<ImportableIntegrationService> | null;
|
||||
|
||||
@ForeignKey(() => Import)
|
||||
@Column(DataType.UUID)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AllowNull, Column, IsDate } from "sequelize-typescript";
|
||||
import ParanoidModel from "./ParanoidModel";
|
||||
|
||||
class ArchivableModel<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors Model base; tightening to object resolves Attributes<M> to never inside Sequelize helpers.
|
||||
TModelAttributes extends object = any,
|
||||
TCreationAttributes extends object = TModelAttributes,
|
||||
> extends ParanoidModel<TModelAttributes, TCreationAttributes> {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import Model from "./Model";
|
||||
|
||||
class IdModel<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors Model base; tightening to object resolves Attributes<M> to never inside Sequelize helpers.
|
||||
TModelAttributes extends object = any,
|
||||
TCreationAttributes extends object = TModelAttributes,
|
||||
> extends Model<TModelAttributes, TCreationAttributes> {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "sequelize-typescript";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import type { Replace, APIContext } from "@server/types";
|
||||
import { getChangsetSkipped } from "../decorators/Changeset";
|
||||
import { getChangesetSkipped } from "../decorators/Changeset";
|
||||
import { InternalError } from "@server/errors";
|
||||
|
||||
type EventOverrideOptions = {
|
||||
@@ -48,6 +48,7 @@ type EventOptions = EventOverrideOptions & {
|
||||
export type HookContext = APIContext["context"] & { event?: EventOptions };
|
||||
|
||||
class Model<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Sequelize's Attributes<M> resolves to never under object default; load-bearing for static helpers like saveWithCtx<M extends Model>.
|
||||
TModelAttributes extends object = any,
|
||||
TCreationAttributes extends object = TModelAttributes,
|
||||
> extends SequelizeModel<TModelAttributes, TCreationAttributes> {
|
||||
@@ -392,7 +393,7 @@ class Model<
|
||||
|
||||
const virtualFields = (this.constructor as typeof Model).virtualFields;
|
||||
const blobFields = (this.constructor as typeof Model).blobFields;
|
||||
const skippedFields = getChangsetSkipped(this);
|
||||
const skippedFields = getChangesetSkipped(this);
|
||||
|
||||
for (const change of changes) {
|
||||
const previous = this.previous(change);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DeletedAt } from "sequelize-typescript";
|
||||
import IdModel from "./IdModel";
|
||||
|
||||
class ParanoidModel<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors Model base; tightening to object resolves Attributes<M> to never inside Sequelize helpers.
|
||||
TModelAttributes extends object = any,
|
||||
TCreationAttributes extends object = TModelAttributes,
|
||||
> extends IdModel<TModelAttributes, TCreationAttributes> {
|
||||
|
||||
@@ -5,7 +5,7 @@ const key = Symbol("skipChangeset");
|
||||
/**
|
||||
* This decorator is used to annotate a property as being skipped from being included in a changeset.
|
||||
*/
|
||||
export function SkipChangeset(target: any, propertyKey: string) {
|
||||
export function SkipChangeset(target: object, propertyKey: string) {
|
||||
const properties: string[] = Reflect.getMetadata(key, target);
|
||||
|
||||
if (!properties) {
|
||||
@@ -18,6 +18,6 @@ export function SkipChangeset(target: any, propertyKey: string) {
|
||||
/**
|
||||
* This function is used to get the properties that should be skipped from a changeset.
|
||||
*/
|
||||
export function getChangsetSkipped(target: any): string[] {
|
||||
export function getChangesetSkipped(target: object): string[] {
|
||||
return Reflect.getMetadata(key, target) || [];
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ export function CounterCache<
|
||||
});
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS rejects PropertyDescriptor as legacy decorator return type; descriptor is consumed by Sequelize at runtime.
|
||||
} as any;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const key = "sequelize:vault";
|
||||
* so that it can be used by getters and setters. Must be accompanied by a
|
||||
* @Column(DataType.BLOB) annotation.
|
||||
*/
|
||||
export default function Encrypted(target: any, propertyKey: string) {
|
||||
export default function Encrypted(target: object, propertyKey: string) {
|
||||
// Ensure that the Encrypted decorator is the first decorator applied to the property, we can check
|
||||
// this by looking at the attributes of the target and checking if the propertyKey is already defined.
|
||||
if (getAttributes(target)[propertyKey]) {
|
||||
@@ -71,5 +71,6 @@ export default function Encrypted(target: any, propertyKey: string) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS rejects PropertyDescriptor return for legacy decorator; descriptor is consumed by Sequelize at runtime.
|
||||
} as any;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
* @param target model class
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- decorator extends a Sequelize Model class with dynamic statics (rawAttributes, associations) that defy precise typing.
|
||||
export default function Fix(target: any): void {
|
||||
return class extends target {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor must accept arbitrary Sequelize Model args.
|
||||
constructor(...args: any[]) {
|
||||
// suppresses warning from here which is not applicable in our typescript
|
||||
// environment: https://github.com/sequelize/sequelize/blob/00ced18c2cb2a8b99ae0ebf5669c124abb4c673d/src/model.js#L99
|
||||
@@ -63,5 +65,6 @@ export default function Fix(target: any): void {
|
||||
});
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mixin class returned in place of original target; consumer expects original class shape.
|
||||
} as any;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { addAttributeOptions } from "sequelize-typescript";
|
||||
/**
|
||||
* A decorator that validates that a string is a fully qualified domain name.
|
||||
*/
|
||||
export default function IsFQDN(target: any, propertyName: string) {
|
||||
export default function IsFQDN(target: object, propertyName: string) {
|
||||
return addAttributeOptions(target, propertyName, {
|
||||
validate: {
|
||||
validDomain(value: string) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { addAttributeOptions } from "sequelize-typescript";
|
||||
/**
|
||||
* A decorator that validates that a string is a valid hex color code.
|
||||
*/
|
||||
export default function IsHexColor(target: any, propertyName: string) {
|
||||
export default function IsHexColor(target: object, propertyName: string) {
|
||||
return addAttributeOptions(target, propertyName, {
|
||||
validate: {
|
||||
validDomain(value: string) {
|
||||
|
||||
@@ -4,7 +4,10 @@ import { addAttributeOptions } from "sequelize-typescript";
|
||||
/**
|
||||
* A decorator that validates that a string is a url or relative path.
|
||||
*/
|
||||
export default function IsUrlOrRelativePath(target: any, propertyName: string) {
|
||||
export default function IsUrlOrRelativePath(
|
||||
target: object,
|
||||
propertyName: string
|
||||
) {
|
||||
return addAttributeOptions(target, propertyName, {
|
||||
validate: {
|
||||
validUrlOrPath(value: string) {
|
||||
|
||||
@@ -13,8 +13,8 @@ export default function Length({
|
||||
msg?: string;
|
||||
min?: number;
|
||||
max: number;
|
||||
}): (target: any, propertyName: string) => void {
|
||||
return (target: any, propertyName: string) =>
|
||||
}): (target: object, propertyName: string) => void {
|
||||
return (target: object, propertyName: string) =>
|
||||
addAttributeOptions(target, propertyName, {
|
||||
validate: {
|
||||
validLength(value: string) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { addAttributeOptions } from "sequelize-typescript";
|
||||
* A decorator that validates that a string does not include something that
|
||||
* looks like a URL.
|
||||
*/
|
||||
export default function NotContainsUrl(target: any, propertyName: string) {
|
||||
export default function NotContainsUrl(target: object, propertyName: string) {
|
||||
return addAttributeOptions(target, propertyName, {
|
||||
validate: {
|
||||
not: {
|
||||
|
||||
@@ -17,8 +17,8 @@ export default function TextLength({
|
||||
msg?: string;
|
||||
min?: number;
|
||||
max: number;
|
||||
}): (target: any, propertyName: string) => void {
|
||||
return (target: any, propertyName: string) =>
|
||||
}): (target: object, propertyName: string) => void {
|
||||
return (target: object, propertyName: string) =>
|
||||
addAttributeOptions(target, propertyName, {
|
||||
validate: {
|
||||
validLength(value: ProsemirrorData) {
|
||||
|
||||
+23
-6
@@ -8,15 +8,32 @@ jest.mock("@server/logging/sentry", () => ({
|
||||
requestErrorHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
type MockCtx = {
|
||||
headers: Record<string, string>;
|
||||
headerSent: boolean;
|
||||
writable: boolean;
|
||||
accepts: jest.Mock;
|
||||
set: jest.Mock;
|
||||
res: { end: jest.Mock };
|
||||
status: number | undefined;
|
||||
type: string | undefined;
|
||||
body: unknown;
|
||||
};
|
||||
|
||||
type ReportableError = Error & {
|
||||
status?: number;
|
||||
isReportable?: boolean;
|
||||
};
|
||||
|
||||
describe("onerror", () => {
|
||||
let app: Koa;
|
||||
let ctx: any;
|
||||
let ctx: MockCtx;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a mock Koa app
|
||||
app = {
|
||||
context: {},
|
||||
} as any;
|
||||
} as unknown as Koa;
|
||||
|
||||
// Apply the onerror middleware
|
||||
onerror(app);
|
||||
@@ -68,7 +85,7 @@ describe("onerror", () => {
|
||||
});
|
||||
|
||||
it("should report unknown errors without isReportable property to Sentry", () => {
|
||||
const error = new Error("Unknown error") as any;
|
||||
const error = new Error("Unknown error") as ReportableError;
|
||||
error.status = 500;
|
||||
|
||||
app.context.onerror.call(ctx, error);
|
||||
@@ -77,7 +94,7 @@ describe("onerror", () => {
|
||||
});
|
||||
|
||||
it("should report errors with invalid status codes to Sentry", () => {
|
||||
const error = new Error("Invalid status error") as any;
|
||||
const error = new Error("Invalid status error") as ReportableError;
|
||||
error.status = 999;
|
||||
|
||||
app.context.onerror.call(ctx, error);
|
||||
@@ -86,7 +103,7 @@ describe("onerror", () => {
|
||||
});
|
||||
|
||||
it("should not report errors explicitly marked with isReportable: false", () => {
|
||||
const error = new Error("Custom error") as any;
|
||||
const error = new Error("Custom error") as ReportableError;
|
||||
error.status = 500;
|
||||
error.isReportable = false;
|
||||
|
||||
@@ -96,7 +113,7 @@ describe("onerror", () => {
|
||||
});
|
||||
|
||||
it("should report errors explicitly marked with isReportable: true", () => {
|
||||
const error = new Error("Custom error") as any;
|
||||
const error = new Error("Custom error") as ReportableError;
|
||||
error.status = 400;
|
||||
error.isReportable = true;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function presentCollection(
|
||||
) {
|
||||
const asData = !ctx || Number(ctx?.headers["x-api-version"] ?? 0) >= 3;
|
||||
|
||||
const res: Record<string, any> = {
|
||||
const res: Record<string, unknown> = {
|
||||
id: collection.id,
|
||||
url: collection.path,
|
||||
urlId: collection.urlId,
|
||||
|
||||
+11
-10
@@ -7,8 +7,11 @@ import type { Document, User, Group } from "@server/models";
|
||||
import { View } from "@server/models";
|
||||
import { opts } from "@server/utils/i18n";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- heterogeneous payload from internal callers and third-party unfurl plugins.
|
||||
type UnfurlData = Record<string, any>;
|
||||
|
||||
async function presentUnfurl(
|
||||
data: Record<string, any>,
|
||||
data: UnfurlData,
|
||||
options?: { includeEmail: boolean }
|
||||
) {
|
||||
switch (data.type) {
|
||||
@@ -30,7 +33,7 @@ async function presentUnfurl(
|
||||
}
|
||||
|
||||
const presentURL = (
|
||||
data: Record<string, any>
|
||||
data: UnfurlData
|
||||
): UnfurlResponse[UnfurlResourceType.URL] => {
|
||||
// TODO: For backwards compatibility, remove once cache has expired in next release.
|
||||
if (data.transformedUnfurl) {
|
||||
@@ -49,7 +52,7 @@ const presentURL = (
|
||||
};
|
||||
|
||||
const presentMention = async (
|
||||
data: Record<string, any>,
|
||||
data: UnfurlData,
|
||||
options?: { includeEmail: boolean }
|
||||
): Promise<UnfurlResponse[UnfurlResourceType.Mention]> => {
|
||||
const user: User = data.user;
|
||||
@@ -69,7 +72,7 @@ const presentMention = async (
|
||||
};
|
||||
|
||||
const presentGroup = async (
|
||||
data: Record<string, any>
|
||||
data: UnfurlData
|
||||
): Promise<UnfurlResponse[UnfurlResourceType.Group]> => {
|
||||
const group: Group = data.group;
|
||||
const memberCount = await group.memberCount;
|
||||
@@ -89,7 +92,7 @@ const presentGroup = async (
|
||||
};
|
||||
|
||||
const presentDocument = (
|
||||
data: Record<string, any>
|
||||
data: UnfurlData
|
||||
): UnfurlResponse[UnfurlResourceType.Document] => {
|
||||
const document: Document = data.document;
|
||||
const viewer: User | undefined = data.viewer;
|
||||
@@ -106,18 +109,16 @@ const presentDocument = (
|
||||
};
|
||||
};
|
||||
|
||||
const presentPR = (
|
||||
data: Record<string, any>
|
||||
): UnfurlResponse[UnfurlResourceType.PR] =>
|
||||
const presentPR = (data: UnfurlData): UnfurlResponse[UnfurlResourceType.PR] =>
|
||||
data as UnfurlResponse[UnfurlResourceType.PR]; // this would have been transformed by the unfurl plugin.
|
||||
|
||||
const presentIssue = (
|
||||
data: Record<string, any>
|
||||
data: UnfurlData
|
||||
): UnfurlResponse[UnfurlResourceType.Issue] =>
|
||||
data as UnfurlResponse[UnfurlResourceType.Issue]; // this would have been transformed by the unfurl plugin.
|
||||
|
||||
const presentProject = (
|
||||
data: Record<string, any>
|
||||
data: UnfurlData
|
||||
): UnfurlResponse[UnfurlResourceType.Project] =>
|
||||
data as UnfurlResponse[UnfurlResourceType.Project]; // this would have been transformed by the unfurl plugin.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import SearchProviderManager from "@server/utils/SearchProviderManager";
|
||||
import SearchIndexProcessor from "./SearchIndexProcessor";
|
||||
|
||||
type PerformArg = Parameters<SearchIndexProcessor["perform"]>[0];
|
||||
|
||||
const processor = new SearchIndexProcessor();
|
||||
|
||||
describe("SearchIndexProcessor", () => {
|
||||
@@ -48,7 +50,7 @@ describe("SearchIndexProcessor", () => {
|
||||
collectionId: collection.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
} as any);
|
||||
} as PerformArg);
|
||||
|
||||
expect(indexSpy).toHaveBeenCalledWith(
|
||||
SearchableModel.Document,
|
||||
@@ -69,7 +71,7 @@ describe("SearchIndexProcessor", () => {
|
||||
collectionId: "some-collection-id",
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
} as any);
|
||||
} as PerformArg);
|
||||
|
||||
expect(removeSpy).toHaveBeenCalledWith(
|
||||
SearchableModel.Document,
|
||||
|
||||
@@ -8,7 +8,7 @@ export enum TaskPriority {
|
||||
High = 10,
|
||||
}
|
||||
|
||||
export abstract class BaseTask<T extends Record<string, any>> {
|
||||
export abstract class BaseTask<T extends object> {
|
||||
/**
|
||||
* Schedule this task type to be processed asynchronously by a worker.
|
||||
*
|
||||
@@ -32,7 +32,7 @@ export abstract class BaseTask<T extends Record<string, any>> {
|
||||
* @param props Properties to be used by the task
|
||||
* @returns A promise that resolves once the task has completed.
|
||||
*/
|
||||
public abstract perform(props: T): Promise<any>;
|
||||
public abstract perform(props: T): Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Handle failure when all attempts are exhausted for the task.
|
||||
|
||||
@@ -35,7 +35,9 @@ describe("#auth.info", () => {
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const availableTeamIds = body.data.availableTeams.map((t: any) => t.id);
|
||||
const availableTeamIds = body.data.availableTeams.map(
|
||||
(t: { id: string }) => t.id
|
||||
);
|
||||
|
||||
expect(availableTeamIds.length).toEqual(3);
|
||||
expect(availableTeamIds).toContain(team.id);
|
||||
|
||||
@@ -834,7 +834,7 @@ describe("#documents.list", () => {
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toHaveLength(2);
|
||||
const docIds = body.data.map((doc: any) => doc.id);
|
||||
const docIds = body.data.map((doc: { id: string }) => doc.id);
|
||||
expect(docIds).toContain(docs[0].id);
|
||||
expect(docIds).toContain(docs[1].id);
|
||||
expect(docIds).not.toContain(docs[2].id);
|
||||
@@ -2033,7 +2033,9 @@ describe("#documents.search", () => {
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toHaveLength(2);
|
||||
const returnedIds = body.data.map((d: any) => d.document.id).sort();
|
||||
const returnedIds = body.data
|
||||
.map((d: { document: { id: string } }) => d.document.id)
|
||||
.sort();
|
||||
const expectedIds = docsInCollection1.map((d) => d.id).sort();
|
||||
expect(returnedIds).toEqual(expectedIds);
|
||||
});
|
||||
@@ -3218,7 +3220,7 @@ describe("#documents.restore", () => {
|
||||
revisionId,
|
||||
},
|
||||
headers: {
|
||||
"x-api-version": 3,
|
||||
"x-api-version": "3",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
@@ -3333,10 +3335,12 @@ describe("#documents.import", () => {
|
||||
collectionId: collection.id,
|
||||
});
|
||||
|
||||
jest.spyOn(FileStorage, "store").mockResolvedValue(undefined as any);
|
||||
jest
|
||||
.spyOn(FileStorage, "store")
|
||||
.mockResolvedValue(undefined as unknown as string);
|
||||
jest.spyOn(DocumentImportTask.prototype, "schedule").mockResolvedValue({
|
||||
finished: jest.fn().mockResolvedValue({ documentId: document.id }),
|
||||
} as any);
|
||||
} as unknown as Awaited<ReturnType<DocumentImportTask["schedule"]>>);
|
||||
|
||||
const content = await readFile(
|
||||
path.resolve(
|
||||
@@ -5765,7 +5769,7 @@ describe("#documents.documents", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(body.data.id).toBe(parent.id);
|
||||
const childIds = body.data.children.map((node: any) => node.id);
|
||||
const childIds = body.data.children.map((node: { id: string }) => node.id);
|
||||
expect(childIds).toContain(child1.id);
|
||||
expect(childIds).toContain(child2.id);
|
||||
});
|
||||
|
||||
@@ -472,7 +472,7 @@ describe("#events.list", () => {
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// admin SHOULD see events for documents without a collection
|
||||
const eventIds = body.data.map((e: any) => e.id);
|
||||
const eventIds = body.data.map((e: { id: string }) => e.id);
|
||||
expect(eventIds).toContain(draftEvent.id);
|
||||
});
|
||||
|
||||
|
||||
@@ -293,11 +293,13 @@ describe("#groups.list", () => {
|
||||
expect(body.data.groupMemberships[0].groupId).toEqual(group.id);
|
||||
expect(body.data.groupMemberships[1].groupId).toEqual(group.id);
|
||||
expect(
|
||||
body.data.groupMemberships.map((u: any) => u.user.id).includes(user.id)
|
||||
body.data.groupMemberships
|
||||
.map((u: { user: { id: string } }) => u.user.id)
|
||||
.includes(user.id)
|
||||
).toBe(true);
|
||||
expect(
|
||||
body.data.groupMemberships
|
||||
.map((u: any) => u.user.id)
|
||||
.map((u: { user: { id: string } }) => u.user.id)
|
||||
.includes(anotherUser.id)
|
||||
).toBe(true);
|
||||
expect(body.policies.length).toEqual(2);
|
||||
@@ -317,11 +319,13 @@ describe("#groups.list", () => {
|
||||
expect(anotherBody.data.groupMemberships[0].groupId).toEqual(group.id);
|
||||
expect(anotherBody.data.groupMemberships[1].groupId).toEqual(group.id);
|
||||
expect(
|
||||
body.data.groupMemberships.map((u: any) => u.user.id).includes(user.id)
|
||||
anotherBody.data.groupMemberships
|
||||
.map((u: { user: { id: string } }) => u.user.id)
|
||||
.includes(user.id)
|
||||
).toBe(true);
|
||||
expect(
|
||||
body.data.groupMemberships
|
||||
.map((u: any) => u.user.id)
|
||||
anotherBody.data.groupMemberships
|
||||
.map((u: { user: { id: string } }) => u.user.id)
|
||||
.includes(anotherUser.id)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,12 @@ import {
|
||||
} from "@server/test/factories";
|
||||
import { getTestServer } from "@server/test/support";
|
||||
|
||||
type NotificationItem = {
|
||||
actor: { id: string };
|
||||
userId: string;
|
||||
event: NotificationEventType;
|
||||
};
|
||||
|
||||
const server = getTestServer();
|
||||
|
||||
describe("#notifications.list", () => {
|
||||
@@ -69,13 +75,15 @@ describe("#notifications.list", () => {
|
||||
expect(body.data.notifications.length).toBe(3);
|
||||
expect(body.pagination.total).toBe(3);
|
||||
expect(body.data.unseen).toBe(2);
|
||||
expect((randomElement(body.data.notifications) as any).actor.id).toBe(
|
||||
actor.id
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).actor.id
|
||||
).toBe(actor.id);
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).userId
|
||||
).toBe(user.id);
|
||||
const events = body.data.notifications.map(
|
||||
(n: NotificationItem) => n.event
|
||||
);
|
||||
expect((randomElement(body.data.notifications) as any).userId).toBe(
|
||||
user.id
|
||||
);
|
||||
const events = body.data.notifications.map((n: any) => n.event);
|
||||
expect(events).toContain(NotificationEventType.UpdateDocument);
|
||||
expect(events).toContain(NotificationEventType.CreateComment);
|
||||
expect(events).toContain(NotificationEventType.MentionedInComment);
|
||||
@@ -134,13 +142,15 @@ describe("#notifications.list", () => {
|
||||
expect(body.data.notifications.length).toBe(1);
|
||||
expect(body.pagination.total).toBe(1);
|
||||
expect(body.data.unseen).toBe(1);
|
||||
expect((randomElement(body.data.notifications) as any).actor.id).toBe(
|
||||
actor.id
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).actor.id
|
||||
).toBe(actor.id);
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).userId
|
||||
).toBe(user.id);
|
||||
const events = body.data.notifications.map(
|
||||
(n: NotificationItem) => n.event
|
||||
);
|
||||
expect((randomElement(body.data.notifications) as any).userId).toBe(
|
||||
user.id
|
||||
);
|
||||
const events = body.data.notifications.map((n: any) => n.event);
|
||||
expect(events).toContain(NotificationEventType.MentionedInComment);
|
||||
});
|
||||
|
||||
@@ -199,13 +209,15 @@ describe("#notifications.list", () => {
|
||||
expect(body.data.notifications.length).toBe(2);
|
||||
expect(body.pagination.total).toBe(2);
|
||||
expect(body.data.unseen).toBe(2);
|
||||
expect((randomElement(body.data.notifications) as any).actor.id).toBe(
|
||||
actor.id
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).actor.id
|
||||
).toBe(actor.id);
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).userId
|
||||
).toBe(user.id);
|
||||
const events = body.data.notifications.map(
|
||||
(n: NotificationItem) => n.event
|
||||
);
|
||||
expect((randomElement(body.data.notifications) as any).userId).toBe(
|
||||
user.id
|
||||
);
|
||||
const events = body.data.notifications.map((n: any) => n.event);
|
||||
expect(events).toContain(NotificationEventType.CreateComment);
|
||||
expect(events).toContain(NotificationEventType.UpdateDocument);
|
||||
});
|
||||
@@ -265,13 +277,15 @@ describe("#notifications.list", () => {
|
||||
expect(body.data.notifications.length).toBe(1);
|
||||
expect(body.pagination.total).toBe(1);
|
||||
expect(body.data.unseen).toBe(1);
|
||||
expect((randomElement(body.data.notifications) as any).actor.id).toBe(
|
||||
actor.id
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).actor.id
|
||||
).toBe(actor.id);
|
||||
expect(
|
||||
randomElement<NotificationItem>(body.data.notifications).userId
|
||||
).toBe(user.id);
|
||||
const events = body.data.notifications.map(
|
||||
(n: NotificationItem) => n.event
|
||||
);
|
||||
expect((randomElement(body.data.notifications) as any).userId).toBe(
|
||||
user.id
|
||||
);
|
||||
const events = body.data.notifications.map((n: any) => n.event);
|
||||
expect(events).toContain(NotificationEventType.MentionedInComment);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -302,10 +302,10 @@ describe("#pins.list", () => {
|
||||
expect(body.data).toBeTruthy();
|
||||
expect(body.data.pins).toBeTruthy();
|
||||
expect(body.data.pins).toHaveLength(2);
|
||||
const pinIds = body.data.pins.map((p: any) => p.id);
|
||||
const pinIds = body.data.pins.map((p: { id: string }) => p.id);
|
||||
expect(pinIds).toContain(pins[0].id);
|
||||
expect(pinIds).toContain(pins[1].id);
|
||||
const docIds = body.data.documents.map((d: any) => d.id);
|
||||
const docIds = body.data.documents.map((d: { id: string }) => d.id);
|
||||
expect(docIds).toContain(docs[0].id);
|
||||
expect(docIds).toContain(docs[1].id);
|
||||
});
|
||||
|
||||
@@ -155,7 +155,9 @@ describe("#relationships.info", () => {
|
||||
expect(body.data.relationship).toBeTruthy();
|
||||
expect(body.data.documents).toHaveLength(2);
|
||||
// User can read their own document but admin document should also be included
|
||||
const documentIds = body.data.documents.map((doc: any) => doc.id);
|
||||
const documentIds = body.data.documents.map(
|
||||
(doc: { id: string }) => doc.id
|
||||
);
|
||||
expect(documentIds).toContain(userDocument.id);
|
||||
});
|
||||
|
||||
@@ -172,7 +174,9 @@ describe("#relationships.info", () => {
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.documents).toHaveLength(2);
|
||||
const documentIds = body.data.documents.map((doc: any) => doc.id);
|
||||
const documentIds = body.data.documents.map(
|
||||
(doc: { id: string }) => doc.id
|
||||
);
|
||||
expect(documentIds).toContain(document.id);
|
||||
expect(documentIds).toContain(reverseDocument.id);
|
||||
});
|
||||
@@ -267,7 +271,7 @@ describe("#relationships.list", () => {
|
||||
expect(body.data.relationships).toBeTruthy();
|
||||
|
||||
// All returned relationships should be backlinks
|
||||
body.data.relationships.forEach((rel: any) => {
|
||||
body.data.relationships.forEach((rel: { type: RelationshipType }) => {
|
||||
expect(rel.type).toEqual(RelationshipType.Backlink);
|
||||
});
|
||||
});
|
||||
@@ -285,7 +289,7 @@ describe("#relationships.list", () => {
|
||||
expect(body.data.relationships).toBeTruthy();
|
||||
|
||||
// All returned relationships should have the specified documentId
|
||||
body.data.relationships.forEach((rel: any) => {
|
||||
body.data.relationships.forEach((rel: { documentId: string }) => {
|
||||
expect(rel.documentId).toEqual(documents[0].id);
|
||||
});
|
||||
});
|
||||
@@ -303,7 +307,7 @@ describe("#relationships.list", () => {
|
||||
expect(body.data.relationships).toBeTruthy();
|
||||
|
||||
// All returned relationships should have the specified reverseDocumentId
|
||||
body.data.relationships.forEach((rel: any) => {
|
||||
body.data.relationships.forEach((rel: { reverseDocumentId: string }) => {
|
||||
expect(rel.reverseDocumentId).toEqual(documents[1].id);
|
||||
});
|
||||
});
|
||||
@@ -322,10 +326,12 @@ describe("#relationships.list", () => {
|
||||
expect(body.data.relationships).toBeTruthy();
|
||||
|
||||
// All returned relationships should match both filters
|
||||
body.data.relationships.forEach((rel: any) => {
|
||||
expect(rel.type).toEqual(RelationshipType.Backlink);
|
||||
expect(rel.documentId).toEqual(documents[0].id);
|
||||
});
|
||||
body.data.relationships.forEach(
|
||||
(rel: { type: RelationshipType; documentId: string }) => {
|
||||
expect(rel.type).toEqual(RelationshipType.Backlink);
|
||||
expect(rel.documentId).toEqual(documents[0].id);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when documentId is invalid", async () => {
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("#searches.list", () => {
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toHaveLength(3);
|
||||
const queries = body.data.map((d: any) => d.query);
|
||||
const queries = body.data.map((d: { query: string }) => d.query);
|
||||
expect(queries).toContain("query");
|
||||
expect(queries).toContain("foo");
|
||||
expect(queries).toContain("bar");
|
||||
|
||||
+27
-16
@@ -3,13 +3,20 @@ import type { AddressInfo } from "node:net";
|
||||
import type Koa from "koa";
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
import nodeFetch from "node-fetch";
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
import type { RequestInit } from "node-fetch";
|
||||
|
||||
type TestRequestOptions = Omit<RequestInit, "body" | "headers"> & {
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
class TestServer {
|
||||
private server: http.Server;
|
||||
private listener?: Promise<void> | null;
|
||||
|
||||
constructor(app: Koa) {
|
||||
this.server = http.createServer(app.callback() as any);
|
||||
this.server = http.createServer(app.callback() as http.RequestListener);
|
||||
}
|
||||
|
||||
get address(): string {
|
||||
@@ -29,19 +36,23 @@ class TestServer {
|
||||
return this.listener;
|
||||
}
|
||||
|
||||
fetch(path: string, opts: any) {
|
||||
fetch(path: string, opts: TestRequestOptions) {
|
||||
return this.listen().then(() => {
|
||||
const url = `${this.address}${path}`;
|
||||
const options = Object.assign({ headers: {} }, opts);
|
||||
const contentType =
|
||||
options.headers["Content-Type"] ?? options.headers["content-type"];
|
||||
const headers: Record<string, string> = { ...opts.headers };
|
||||
let body = opts.body;
|
||||
const contentType = headers["Content-Type"] ?? headers["content-type"];
|
||||
// automatic JSON encoding
|
||||
if (!contentType && typeof options.body === "object") {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
options.body = JSON.stringify(options.body);
|
||||
if (!contentType && typeof body === "object" && body !== null) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return nodeFetch(url, options);
|
||||
return nodeFetch(url, {
|
||||
...opts,
|
||||
headers,
|
||||
body: body as string | undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,31 +62,31 @@ class TestServer {
|
||||
this.server.close();
|
||||
}
|
||||
|
||||
delete(path: string, options?: any) {
|
||||
delete(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "DELETE" });
|
||||
}
|
||||
|
||||
get(path: string, options?: any) {
|
||||
get(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "GET" });
|
||||
}
|
||||
|
||||
head(path: string, options?: any) {
|
||||
head(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "HEAD" });
|
||||
}
|
||||
|
||||
options(path: string, options?: any) {
|
||||
options(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "OPTIONS" });
|
||||
}
|
||||
|
||||
patch(path: string, options?: any) {
|
||||
patch(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "PATCH" });
|
||||
}
|
||||
|
||||
post(path: string, options?: any) {
|
||||
post(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "POST" });
|
||||
}
|
||||
|
||||
put(path: string, options?: any) {
|
||||
put(path: string, options?: TestRequestOptions) {
|
||||
return this.fetch(path, { ...options, method: "PUT" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ export enum Hook {
|
||||
type PluginValueMap = {
|
||||
[Hook.API]: Router;
|
||||
[Hook.AuthProvider]: { router: Router | Promise<Router>; id: string };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- typeof BaseEmail<EmailProps> isn't assignable from BaseEmail<Subtype>; plugins register heterogeneous template Props.
|
||||
[Hook.EmailTemplate]: typeof BaseEmail<any>;
|
||||
[Hook.IssueProvider]: BaseIssueProvider;
|
||||
[Hook.Processor]: typeof BaseProcessor;
|
||||
[Hook.SearchProvider]: BaseSearchProvider;
|
||||
[Hook.Task]: typeof BaseTask<any>;
|
||||
[Hook.Task]: typeof BaseTask<object>;
|
||||
[Hook.Uninstall]: UninstallSignature;
|
||||
[Hook.UnfurlProvider]: { unfurl: UnfurlSignature; cacheExpiry: number };
|
||||
[Hook.GroupSyncProvider]: { id: string; provider: GroupSyncProvider };
|
||||
|
||||
@@ -7,7 +7,7 @@ const key = Symbol("env:public");
|
||||
/**
|
||||
* This decorator on an environment variable makes that variable available client-side
|
||||
*/
|
||||
export function Public(target: any, propertyKey: string) {
|
||||
export function Public(target: object, propertyKey: string) {
|
||||
const publicVars: string[] = Reflect.getMetadata(key, target);
|
||||
|
||||
if (!publicVars) {
|
||||
@@ -18,6 +18,7 @@ export function Public(target: any, propertyKey: string) {
|
||||
}
|
||||
|
||||
export class PublicEnvironmentRegister {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- consumed at runtime as a flat map of typed Environment values; tightening to unknown breaks call sites.
|
||||
private static publicEnv: Record<string, any> = {};
|
||||
|
||||
static registerEnv(env: Environment) {
|
||||
|
||||
@@ -135,11 +135,15 @@ describe("isDatabaseUrl", () => {
|
||||
});
|
||||
|
||||
it("should reject null", () => {
|
||||
expect(isDatabaseUrl(null as any, defaultOptions)).toBe(false);
|
||||
expect(isDatabaseUrl(null as unknown as string, defaultOptions)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject undefined", () => {
|
||||
expect(isDatabaseUrl(undefined as any, defaultOptions)).toBe(false);
|
||||
expect(
|
||||
isDatabaseUrl(undefined as unknown as string, defaultOptions)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should reject URL with invalid protocol", () => {
|
||||
@@ -279,9 +283,7 @@ describe("isMailboxAddress", () => {
|
||||
});
|
||||
|
||||
it("should accept a mailbox format with quoted display name containing a comma", () => {
|
||||
expect(
|
||||
isMailboxAddress('"Company, Inc." <user@example.com>')
|
||||
).toBe(true);
|
||||
expect(isMailboxAddress('"Company, Inc." <user@example.com>')).toBe(true);
|
||||
});
|
||||
|
||||
it("should accept a mailbox format with a quoted display name", () => {
|
||||
|
||||
Reference in New Issue
Block a user