mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
chore: Migrate to vitest (#12272)
* wip * Remove obsolete snapshots * simplify * chore(test): Convert mocks to TypeScript and tighten fetch mock types Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove unneccessary patches * Migrate to msw instead of custom fetch mock * Address PR review comments - Split chained vi.useFakeTimers().setSystemTime() into separate calls. - Switch test setup to dynamic imports so EventEmitter.defaultMaxListeners assignment runs before module init (static imports were hoisted above it). - Drop redundant NODE_ENV guard in monkeyPatchSequelizeErrorsForJest; its sole caller already gates on env.isTest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ jobs:
|
|||||||
config:
|
config:
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
- 'vite.config.ts'
|
- 'vite.config.ts'
|
||||||
|
- 'vitest.config.ts'
|
||||||
server:
|
server:
|
||||||
- 'server/**'
|
- 'server/**'
|
||||||
- 'shared/**'
|
- 'shared/**'
|
||||||
@@ -82,15 +83,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
- name: Restore Jest transform cache
|
- run: yarn test:${{ matrix.test-group }}
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: /tmp/jest_runner
|
|
||||||
key: jest-${{ matrix.test-group }}-${{ hashFiles('.babelrc', '.jestconfig.json', 'yarn.lock', 'app/**/*.ts', 'app/**/*.tsx', 'shared/**/*.ts', 'shared/**/*.tsx') }}
|
|
||||||
restore-keys: |
|
|
||||||
jest-${{ matrix.test-group }}-${{ hashFiles('.babelrc', '.jestconfig.json', 'yarn.lock') }}-
|
|
||||||
jest-${{ matrix.test-group }}-
|
|
||||||
- run: yarn test:${{ matrix.test-group }} --cacheDirectory=/tmp/jest_runner
|
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
needs: changes
|
needs: changes
|
||||||
@@ -118,17 +111,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
- name: Restore Jest transform cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: /tmp/jest_runner
|
|
||||||
key: jest-server-${{ matrix.shard }}-${{ hashFiles('.babelrc', '.jestconfig.json', 'yarn.lock', 'server/**/*.ts', 'shared/**/*.ts', 'plugins/**/*.ts') }}
|
|
||||||
restore-keys: |
|
|
||||||
jest-server-${{ matrix.shard }}-${{ hashFiles('.babelrc', '.jestconfig.json', 'yarn.lock') }}-
|
|
||||||
jest-server-${{ matrix.shard }}-
|
|
||||||
- run: yarn sequelize db:migrate
|
- run: yarn sequelize db:migrate
|
||||||
- name: Run server tests
|
- name: Run server tests
|
||||||
run: yarn test:server --maxWorkers=2 --shard=${{ matrix.shard }}/4 --cacheDirectory=/tmp/jest_runner
|
run: yarn test:server --maxWorkers=2 --shard=${{ matrix.shard }}/4
|
||||||
|
|
||||||
bundle-size:
|
bundle-size:
|
||||||
needs: changes
|
needs: changes
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"workerIdleMemoryLimit": "0.75",
|
|
||||||
"maxWorkers": "50%",
|
|
||||||
"transformIgnorePatterns": ["node_modules/(?!(franc|trigram-utils)/)"],
|
|
||||||
"projects": [
|
|
||||||
{
|
|
||||||
"displayName": "server",
|
|
||||||
"roots": ["<rootDir>/server", "<rootDir>/plugins"],
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"^@server/(.*)$": "<rootDir>/server/$1",
|
|
||||||
"^@shared/(.*)$": "<rootDir>/shared/$1"
|
|
||||||
},
|
|
||||||
"setupFiles": [
|
|
||||||
"<rootDir>/__mocks__/console.js",
|
|
||||||
"<rootDir>/server/test/setupMocks.js"
|
|
||||||
],
|
|
||||||
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
|
|
||||||
"globalTeardown": "<rootDir>/server/test/globalTeardown.js",
|
|
||||||
"testEnvironment": "node"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"displayName": "app",
|
|
||||||
"roots": ["<rootDir>/app"],
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"^~/(.*)$": "<rootDir>/app/$1",
|
|
||||||
"^@shared/(.*)$": "<rootDir>/shared/$1",
|
|
||||||
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
|
|
||||||
},
|
|
||||||
"modulePaths": ["<rootDir>/app"],
|
|
||||||
"setupFiles": ["<rootDir>/__mocks__/window.js"],
|
|
||||||
"setupFilesAfterEnv": ["<rootDir>/app/test/setup.ts"],
|
|
||||||
"testEnvironment": "jsdom",
|
|
||||||
"testEnvironmentOptions": {
|
|
||||||
"url": "http://localhost"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"displayName": "shared-node",
|
|
||||||
"roots": ["<rootDir>/shared"],
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"^@server/(.*)$": "<rootDir>/server/$1",
|
|
||||||
"^@shared/(.*)$": "<rootDir>/shared/$1"
|
|
||||||
},
|
|
||||||
"setupFiles": ["<rootDir>/__mocks__/console.js"],
|
|
||||||
"setupFilesAfterEnv": ["<rootDir>/shared/test/setup.ts"],
|
|
||||||
"testEnvironment": "node"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"displayName": "shared-jsdom",
|
|
||||||
"roots": ["<rootDir>/shared"],
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"^~/(.*)$": "<rootDir>/app/$1",
|
|
||||||
"^@shared/(.*)$": "<rootDir>/shared/$1",
|
|
||||||
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
|
|
||||||
},
|
|
||||||
"setupFiles": ["<rootDir>/__mocks__/window.js"],
|
|
||||||
"testEnvironment": "jsdom",
|
|
||||||
"testEnvironmentOptions": {
|
|
||||||
"url": "http://localhost"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+3
-3
@@ -1,4 +1,6 @@
|
|||||||
/* oxlint-disable */
|
/* oxlint-disable */
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { vi } from "vitest";
|
||||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '../.... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '../.... Remove this comment to see the full error message
|
||||||
import localStorage from "../../__mocks__/localStorage";
|
import localStorage from "../../__mocks__/localStorage";
|
||||||
import { initI18n } from "../utils/i18n";
|
import { initI18n } from "../utils/i18n";
|
||||||
@@ -7,6 +9,4 @@ initI18n();
|
|||||||
|
|
||||||
global.localStorage = localStorage;
|
global.localStorage = localStorage;
|
||||||
|
|
||||||
require("jest-fetch-mock").enableMocks();
|
vi.mock("~/utils/ApiClient");
|
||||||
|
|
||||||
jest.mock("~/utils/ApiClient");
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* oxlint-disable */
|
/* oxlint-disable */
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
export const client = {
|
export const client = {
|
||||||
post: jest.fn(() =>
|
post: vi.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: {
|
data: {
|
||||||
user: {},
|
user: {},
|
||||||
|
|||||||
+14
-10
@@ -28,10 +28,11 @@
|
|||||||
"db:rollback": "sequelize db:migrate:undo",
|
"db:rollback": "sequelize db:migrate:undo",
|
||||||
"db:reset": "sequelize db:drop && sequelize db:create && sequelize db:migrate",
|
"db:reset": "sequelize db:drop && sequelize db:create && sequelize db:migrate",
|
||||||
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
|
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
|
||||||
"test": "TZ=UTC jest --config=.jestconfig.json",
|
"test": "TZ=UTC vitest run",
|
||||||
"test:app": "TZ=UTC jest --config=.jestconfig.json --selectProjects app",
|
"test:app": "TZ=UTC vitest run --project app",
|
||||||
"test:shared": "TZ=UTC jest --config=.jestconfig.json --selectProjects shared-node shared-jsdom",
|
"test:shared": "TZ=UTC vitest run --project shared-node --project shared-jsdom",
|
||||||
"test:server": "TZ=UTC jest --config=.jestconfig.json --selectProjects server",
|
"test:server": "TZ=UTC vitest run --project server",
|
||||||
|
"test:watch": "TZ=UTC vitest",
|
||||||
"vite:dev": "VITE_CJS_IGNORE_WARNING=true vite",
|
"vite:dev": "VITE_CJS_IGNORE_WARNING=true vite",
|
||||||
"vite:build": "VITE_CJS_IGNORE_WARNING=true vite build",
|
"vite:build": "VITE_CJS_IGNORE_WARNING=true vite build",
|
||||||
"vite:preview": "VITE_CJS_IGNORE_WARNING=true vite preview"
|
"vite:preview": "VITE_CJS_IGNORE_WARNING=true vite preview"
|
||||||
@@ -283,6 +284,7 @@
|
|||||||
"@babel/preset-typescript": "^7.28.5",
|
"@babel/preset-typescript": "^7.28.5",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@relative-ci/agent": "^4.3.1",
|
"@relative-ci/agent": "^4.3.1",
|
||||||
|
"@swc/core": "^1.15.32",
|
||||||
"@types/addressparser": "^1.0.3",
|
"@types/addressparser": "^1.0.3",
|
||||||
"@types/cookie": "0.6.0",
|
"@types/cookie": "0.6.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
@@ -298,8 +300,8 @@
|
|||||||
"@types/google.analytics": "^0.0.46",
|
"@types/google.analytics": "^0.0.46",
|
||||||
"@types/invariant": "^2.2.37",
|
"@types/invariant": "^2.2.37",
|
||||||
"@types/ioredis-mock": "^8.2.7",
|
"@types/ioredis-mock": "^8.2.7",
|
||||||
"@types/jest": "^29.5.14",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/jsdom": "^28.0.1",
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
"@types/katex": "^0.16.8",
|
"@types/katex": "^0.16.8",
|
||||||
"@types/koa": "^2.15.0",
|
"@types/koa": "^2.15.0",
|
||||||
@@ -346,7 +348,7 @@
|
|||||||
"@types/utf8": "^3.0.3",
|
"@types/utf8": "^3.0.3",
|
||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"babel-jest": "^30.3.0",
|
"@vitest/ui": "^4.1.5",
|
||||||
"babel-plugin-module-resolver": "^5.0.3",
|
"babel-plugin-module-resolver": "^5.0.3",
|
||||||
"babel-plugin-styled-components": "^2.1.4",
|
"babel-plugin-styled-components": "^2.1.4",
|
||||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||||
@@ -357,10 +359,8 @@
|
|||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"i18next-parser": "^8.13.0",
|
"i18next-parser": "^8.13.0",
|
||||||
"ioredis-mock": "^8.13.1",
|
"ioredis-mock": "^8.13.1",
|
||||||
"jest-cli": "^30.3.0",
|
|
||||||
"jest-environment-jsdom": "^30.3.0",
|
|
||||||
"jest-fetch-mock": "^3.0.3",
|
|
||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^16.4.0",
|
||||||
|
"msw": "^2.14.2",
|
||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
"oxlint": "1.50.0",
|
"oxlint": "1.50.0",
|
||||||
"oxlint-tsgolint": "0.14.2",
|
"oxlint-tsgolint": "0.14.2",
|
||||||
@@ -370,7 +370,11 @@
|
|||||||
"rimraf": "^6.1.3",
|
"rimraf": "^6.1.3",
|
||||||
"rollup-plugin-webpack-stats": "2.1.11",
|
"rollup-plugin-webpack-stats": "2.1.11",
|
||||||
"terser": "^5.44.1",
|
"terser": "^5.44.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3",
|
||||||
|
"unplugin-swc": "^1.5.9",
|
||||||
|
"vite-plugin-babel": "^1.6.0",
|
||||||
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "17.0.91",
|
"@types/react": "17.0.91",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respond with redirect location when user is SSO enabled", async () => {
|
it("should respond with redirect location when user is SSO enabled", async () => {
|
||||||
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
|
const spy = vi.spyOn(WelcomeEmail.prototype, "schedule");
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
const team = await buildTeam({ subdomain });
|
const team = await buildTeam({ subdomain });
|
||||||
const user = await buildUser({ teamId: team.id });
|
const user = await buildUser({ teamId: team.id });
|
||||||
@@ -48,7 +48,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respond with success and email to be sent when user has SSO but disabled", async () => {
|
it("should respond with success and email to be sent when user has SSO but disabled", async () => {
|
||||||
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
|
const spy = vi.spyOn(SigninEmail.prototype, "schedule");
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
const team = await buildTeam({ subdomain });
|
const team = await buildTeam({ subdomain });
|
||||||
const user = await buildUser({ teamId: team.id });
|
const user = await buildUser({ teamId: team.id });
|
||||||
@@ -83,7 +83,7 @@ describe("email", () => {
|
|||||||
|
|
||||||
it("should not send email when user is on another subdomain but respond with success", async () => {
|
it("should not send email when user is on another subdomain but respond with success", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
|
const spy = vi.spyOn(WelcomeEmail.prototype, "schedule");
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
await buildTeam({ subdomain });
|
await buildTeam({ subdomain });
|
||||||
const res = await server.post("/auth/email", {
|
const res = await server.post("/auth/email", {
|
||||||
@@ -103,7 +103,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respond with success and email to be sent when user is not SSO enabled", async () => {
|
it("should respond with success and email to be sent when user is not SSO enabled", async () => {
|
||||||
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
|
const spy = vi.spyOn(SigninEmail.prototype, "schedule");
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
const team = await buildTeam({ subdomain });
|
const team = await buildTeam({ subdomain });
|
||||||
const user = await buildGuestUser({
|
const user = await buildGuestUser({
|
||||||
@@ -125,7 +125,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respond with success regardless of whether successful to prevent crawling email logins", async () => {
|
it("should respond with success regardless of whether successful to prevent crawling email logins", async () => {
|
||||||
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
|
const spy = vi.spyOn(WelcomeEmail.prototype, "schedule");
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
await buildTeam({ subdomain });
|
await buildTeam({ subdomain });
|
||||||
const res = await server.post("/auth/email", {
|
const res = await server.post("/auth/email", {
|
||||||
@@ -145,7 +145,7 @@ describe("email", () => {
|
|||||||
|
|
||||||
describe("with multiple users matching email", () => {
|
describe("with multiple users matching email", () => {
|
||||||
it("should default to current subdomain with SSO", async () => {
|
it("should default to current subdomain with SSO", async () => {
|
||||||
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
|
const spy = vi.spyOn(SigninEmail.prototype, "schedule");
|
||||||
const email = "sso-user@example.org";
|
const email = "sso-user@example.org";
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
const team = await buildTeam({
|
const team = await buildTeam({
|
||||||
@@ -174,7 +174,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should default to current subdomain with guest email", async () => {
|
it("should default to current subdomain with guest email", async () => {
|
||||||
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
|
const spy = vi.spyOn(SigninEmail.prototype, "schedule");
|
||||||
const email = "guest-user@example.org";
|
const email = "guest-user@example.org";
|
||||||
const subdomain = faker.internet.domainWord();
|
const subdomain = faker.internet.domainWord();
|
||||||
const team = await buildTeam({
|
const team = await buildTeam({
|
||||||
@@ -203,7 +203,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should default to custom domain with SSO", async () => {
|
it("should default to custom domain with SSO", async () => {
|
||||||
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
|
const spy = vi.spyOn(WelcomeEmail.prototype, "schedule");
|
||||||
const email = "sso-user-2@example.org";
|
const email = "sso-user-2@example.org";
|
||||||
const domain = faker.internet.domainName();
|
const domain = faker.internet.domainName();
|
||||||
const team = await buildTeam({
|
const team = await buildTeam({
|
||||||
@@ -232,7 +232,7 @@ describe("email", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should default to custom domain with guest email", async () => {
|
it("should default to custom domain with guest email", async () => {
|
||||||
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
|
const spy = vi.spyOn(SigninEmail.prototype, "schedule");
|
||||||
const email = "guest-user-2@example.org";
|
const email = "guest-user-2@example.org";
|
||||||
const domain = faker.internet.domainName();
|
const domain = faker.internet.domainName();
|
||||||
const team = await buildTeam({
|
const team = await buildTeam({
|
||||||
|
|||||||
@@ -2,12 +2,19 @@ import { Node } from "prosemirror-model";
|
|||||||
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
|
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
|
||||||
import nodesWithEmptyTextNode from "@server/test/fixtures/notion-page-with-empty-text-nodes.json";
|
import nodesWithEmptyTextNode from "@server/test/fixtures/notion-page-with-empty-text-nodes.json";
|
||||||
import allNodes from "@server/test/fixtures/notion-page.json";
|
import allNodes from "@server/test/fixtures/notion-page.json";
|
||||||
|
import type { ProsemirrorData, ProsemirrorDoc } from "@shared/types";
|
||||||
import type { NotionPage } from "./NotionConverter";
|
import type { NotionPage } from "./NotionConverter";
|
||||||
import { NotionConverter } from "./NotionConverter";
|
import { NotionConverter } from "./NotionConverter";
|
||||||
|
|
||||||
jest.mock("node:crypto", () => ({
|
const generatedId = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
randomUUID: jest.fn(() => "550e8400-e29b-41d4-a716-446655440000"),
|
|
||||||
}));
|
function normalizeGeneratedIds(node: ProsemirrorDoc | ProsemirrorData) {
|
||||||
|
if (node.type === "container_toggle" && node.attrs) {
|
||||||
|
node.attrs.id = generatedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.content?.forEach(normalizeGeneratedIds);
|
||||||
|
}
|
||||||
|
|
||||||
describe("NotionConverter", () => {
|
describe("NotionConverter", () => {
|
||||||
it("converts a page", () => {
|
it("converts a page", () => {
|
||||||
@@ -15,6 +22,7 @@ describe("NotionConverter", () => {
|
|||||||
children: allNodes,
|
children: allNodes,
|
||||||
} as NotionPage);
|
} as NotionPage);
|
||||||
|
|
||||||
|
normalizeGeneratedIds(response);
|
||||||
expect(response).toMatchSnapshot();
|
expect(response).toMatchSnapshot();
|
||||||
expect(ProsemirrorHelper.toProsemirror(response)).toBeInstanceOf(Node);
|
expect(ProsemirrorHelper.toProsemirror(response)).toBeInstanceOf(Node);
|
||||||
});
|
});
|
||||||
@@ -24,6 +32,7 @@ describe("NotionConverter", () => {
|
|||||||
children: nodesWithEmptyTextNode,
|
children: nodesWithEmptyTextNode,
|
||||||
} as NotionPage);
|
} as NotionPage);
|
||||||
|
|
||||||
|
normalizeGeneratedIds(response);
|
||||||
expect(response).toMatchSnapshot();
|
expect(response).toMatchSnapshot();
|
||||||
expect(ProsemirrorHelper.toProsemirror(response)).toBeInstanceOf(Node);
|
expect(ProsemirrorHelper.toProsemirror(response)).toBeInstanceOf(Node);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`NotionConverter converts a page 1`] = `
|
exports[`NotionConverter > converts a page 1`] = `
|
||||||
{
|
{
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
@@ -2093,7 +2093,7 @@ exports[`NotionConverter converts a page 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`NotionConverter converts a page with empty text nodes 1`] = `
|
exports[`NotionConverter > converts a page with empty text nodes 1`] = `
|
||||||
{
|
{
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import {
|
||||||
|
http,
|
||||||
|
HttpResponse,
|
||||||
|
type DefaultBodyType,
|
||||||
|
type StrictRequest,
|
||||||
|
} from "msw";
|
||||||
|
import { server } from "@server/test/msw";
|
||||||
import { fetchOIDCConfiguration } from "./oidcDiscovery";
|
import { fetchOIDCConfiguration } from "./oidcDiscovery";
|
||||||
|
|
||||||
beforeEach(() => {
|
const captureRequest = (url: string, response: Response | (() => Response)) => {
|
||||||
fetchMock.resetMocks();
|
const captured: { request?: StrictRequest<DefaultBodyType> } = {};
|
||||||
});
|
server.use(
|
||||||
|
http.get(url, ({ request }) => {
|
||||||
|
captured.request = request;
|
||||||
|
return typeof response === "function" ? response() : response;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return captured;
|
||||||
|
};
|
||||||
|
|
||||||
describe("fetchOIDCConfiguration", () => {
|
describe("fetchOIDCConfiguration", () => {
|
||||||
it("should fetch and parse OIDC configuration successfully", async () => {
|
it("should fetch and parse OIDC configuration successfully", async () => {
|
||||||
@@ -19,20 +32,18 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
grant_types_supported: ["authorization_code"],
|
grant_types_supported: ["authorization_code"],
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
|
const captured = captureRequest(
|
||||||
|
"https://example.com/.well-known/openid-configuration",
|
||||||
|
() => HttpResponse.json(mockConfig)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await fetchOIDCConfiguration("https://example.com");
|
const result = await fetchOIDCConfiguration("https://example.com");
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured.request?.url).toBe(
|
||||||
"https://example.com/.well-known/openid-configuration",
|
"https://example.com/.well-known/openid-configuration"
|
||||||
expect.objectContaining({
|
|
||||||
method: "GET",
|
|
||||||
headers: expect.objectContaining({
|
|
||||||
Accept: "application/json",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
expect(captured.request?.method).toBe("GET");
|
||||||
|
expect(captured.request?.headers.get("Accept")).toBe("application/json");
|
||||||
expect(result).toEqual(mockConfig);
|
expect(result).toEqual(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,26 +55,37 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
userinfo_endpoint: "https://example.com/userinfo",
|
userinfo_endpoint: "https://example.com/userinfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
|
const captured = captureRequest(
|
||||||
|
"https://example.com/.well-known/openid-configuration",
|
||||||
|
() => HttpResponse.json(mockConfig)
|
||||||
|
);
|
||||||
|
|
||||||
await fetchOIDCConfiguration("https://example.com/");
|
await fetchOIDCConfiguration("https://example.com/");
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured.request?.url).toBe(
|
||||||
"https://example.com/.well-known/openid-configuration",
|
"https://example.com/.well-known/openid-configuration"
|
||||||
expect.any(Object)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error when HTTP request fails", async () => {
|
it("should throw error when HTTP request fails", async () => {
|
||||||
fetchMock.mockRejectOnce(new Error("Network error"));
|
server.use(
|
||||||
|
http.get("https://example.com/.well-known/openid-configuration", () =>
|
||||||
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
HttpResponse.error()
|
||||||
"Network error"
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fetchOIDCConfiguration("https://example.com")
|
||||||
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error when response is not ok", async () => {
|
it("should throw error when response is not ok", async () => {
|
||||||
fetchMock.mockResponseOnce("Not Found", { status: 404 });
|
server.use(
|
||||||
|
http.get(
|
||||||
|
"https://example.com/.well-known/openid-configuration",
|
||||||
|
() => new HttpResponse("Not Found", { status: 404 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
||||||
"Failed to fetch OIDC configuration: 404 Not Found"
|
"Failed to fetch OIDC configuration: 404 Not Found"
|
||||||
@@ -77,7 +99,11 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
// Missing token_endpoint and userinfo_endpoint
|
// Missing token_endpoint and userinfo_endpoint
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(incompleteConfig));
|
server.use(
|
||||||
|
http.get("https://example.com/.well-known/openid-configuration", () =>
|
||||||
|
HttpResponse.json(incompleteConfig)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
||||||
"Missing token_endpoint in OIDC configuration"
|
"Missing token_endpoint in OIDC configuration"
|
||||||
@@ -91,7 +117,11 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
userinfo_endpoint: "https://example.com/userinfo",
|
userinfo_endpoint: "https://example.com/userinfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(configMissingAuth));
|
server.use(
|
||||||
|
http.get("https://example.com/.well-known/openid-configuration", () =>
|
||||||
|
HttpResponse.json(configMissingAuth)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
await expect(fetchOIDCConfiguration("https://example.com")).rejects.toThrow(
|
||||||
"Missing authorization_endpoint in OIDC configuration"
|
"Missing authorization_endpoint in OIDC configuration"
|
||||||
@@ -108,22 +138,20 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
"https://auth.example.com/application/o/outline/userinfo",
|
"https://auth.example.com/application/o/outline/userinfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
|
const captured = captureRequest(
|
||||||
|
"https://auth.example.com/application/o/outline/.well-known/openid-configuration",
|
||||||
|
() => HttpResponse.json(mockConfig)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await fetchOIDCConfiguration(
|
const result = await fetchOIDCConfiguration(
|
||||||
"https://auth.example.com/application/o/outline/"
|
"https://auth.example.com/application/o/outline/"
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured.request?.url).toBe(
|
||||||
"https://auth.example.com/application/o/outline/.well-known/openid-configuration",
|
"https://auth.example.com/application/o/outline/.well-known/openid-configuration"
|
||||||
expect.objectContaining({
|
|
||||||
method: "GET",
|
|
||||||
headers: expect.objectContaining({
|
|
||||||
Accept: "application/json",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
expect(captured.request?.method).toBe("GET");
|
||||||
|
expect(captured.request?.headers.get("Accept")).toBe("application/json");
|
||||||
expect(result).toEqual(mockConfig);
|
expect(result).toEqual(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,22 +165,20 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
"https://auth.example.com/application/o/outline/userinfo",
|
"https://auth.example.com/application/o/outline/userinfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
|
const captured = captureRequest(
|
||||||
|
"https://auth.example.com/application/o/outline/.well-known/openid-configuration",
|
||||||
|
() => HttpResponse.json(mockConfig)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await fetchOIDCConfiguration(
|
const result = await fetchOIDCConfiguration(
|
||||||
"https://auth.example.com/application/o/outline"
|
"https://auth.example.com/application/o/outline"
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured.request?.url).toBe(
|
||||||
"https://auth.example.com/application/o/outline/.well-known/openid-configuration",
|
"https://auth.example.com/application/o/outline/.well-known/openid-configuration"
|
||||||
expect.objectContaining({
|
|
||||||
method: "GET",
|
|
||||||
headers: expect.objectContaining({
|
|
||||||
Accept: "application/json",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
expect(captured.request?.method).toBe("GET");
|
||||||
|
expect(captured.request?.headers.get("Accept")).toBe("application/json");
|
||||||
expect(result).toEqual(mockConfig);
|
expect(result).toEqual(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,17 +190,18 @@ describe("fetchOIDCConfiguration", () => {
|
|||||||
userinfo_endpoint: "https://example.com/userinfo",
|
userinfo_endpoint: "https://example.com/userinfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
|
const captured = captureRequest(
|
||||||
|
"https://example.com/.well-known/openid-configuration",
|
||||||
|
() => HttpResponse.json(mockConfig)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await fetchOIDCConfiguration(
|
const result = await fetchOIDCConfiguration(
|
||||||
"https://example.com/.well-known/openid-configuration"
|
"https://example.com/.well-known/openid-configuration"
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured.request?.url).toBe(
|
||||||
"https://example.com/.well-known/openid-configuration",
|
"https://example.com/.well-known/openid-configuration"
|
||||||
expect.any(Object)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual(mockConfig);
|
expect(result).toEqual(mockConfig);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import PostgresSearchProvider from "./PostgresSearchProvider";
|
|||||||
const provider = SearchProviderManager.getProvider();
|
const provider = SearchProviderManager.getProvider();
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
await buildDocument();
|
await buildDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import { getTestServer } from "@server/test/support";
|
|||||||
import env from "../env";
|
import env from "../env";
|
||||||
import * as Slack from "../slack";
|
import * as Slack from "../slack";
|
||||||
|
|
||||||
jest.mock("../slack", () => ({
|
|
||||||
post: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const server = getTestServer();
|
const server = getTestServer();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(Slack, "post").mockResolvedValue({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe("#hooks.unfurl", () => {
|
describe("#hooks.unfurl", () => {
|
||||||
it("should return documents with matching SSO user", async () => {
|
it("should return documents with matching SSO user", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
const server = getTestServer();
|
const server = getTestServer();
|
||||||
|
|
||||||
// Increase timeout for all tests in this file
|
// Increase timeout for all tests in this file
|
||||||
jest.setTimeout(10000);
|
vi.setConfig({ testTimeout: 10000 });
|
||||||
|
|
||||||
describe("#files.create", () => {
|
describe("#files.create", () => {
|
||||||
it("should fail with status 400 bad request if key is invalid", async () => {
|
it("should fail with status 400 bad request if key is invalid", async () => {
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { buildUser, buildWebhookSubscription } from "@server/test/factories";
|
import { buildUser, buildWebhookSubscription } from "@server/test/factories";
|
||||||
|
import { mockTaskSchedule } from "@server/test/support";
|
||||||
import type { UserEvent } from "@server/types";
|
import type { UserEvent } from "@server/types";
|
||||||
import DeliverWebhookTask from "../tasks/DeliverWebhookTask";
|
|
||||||
import WebhookProcessor from "./WebhookProcessor";
|
import WebhookProcessor from "./WebhookProcessor";
|
||||||
|
|
||||||
jest.mock("../tasks/DeliverWebhookTask");
|
|
||||||
const ip = "127.0.0.1";
|
const ip = "127.0.0.1";
|
||||||
const schedule = jest.fn();
|
const schedule = mockTaskSchedule();
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
DeliverWebhookTask.prototype.schedule = schedule;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("WebhookProcessor", () => {
|
describe("WebhookProcessor", () => {
|
||||||
it("it schedules a delivery for the event", async () => {
|
it("it schedules a delivery for the event", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import {
|
||||||
|
http,
|
||||||
|
HttpResponse,
|
||||||
|
type DefaultBodyType,
|
||||||
|
type StrictRequest,
|
||||||
|
} from "msw";
|
||||||
|
import { server } from "@server/test/msw";
|
||||||
import { WebhookDelivery } from "@server/models";
|
import { WebhookDelivery } from "@server/models";
|
||||||
import {
|
import {
|
||||||
buildUser,
|
buildUser,
|
||||||
@@ -8,14 +14,28 @@ import {
|
|||||||
import type { UserEvent } from "@server/types";
|
import type { UserEvent } from "@server/types";
|
||||||
import DeliverWebhookTask from "./DeliverWebhookTask";
|
import DeliverWebhookTask from "./DeliverWebhookTask";
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.doMock();
|
|
||||||
});
|
|
||||||
|
|
||||||
const ip = "127.0.0.1";
|
const ip = "127.0.0.1";
|
||||||
|
|
||||||
|
type CapturedRequest = {
|
||||||
|
request: StrictRequest<DefaultBodyType>;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const captureWebhook = (
|
||||||
|
url: string,
|
||||||
|
response: () => Response = () => new HttpResponse(null, { status: 200 })
|
||||||
|
) => {
|
||||||
|
const captured: CapturedRequest[] = [];
|
||||||
|
server.use(
|
||||||
|
http.post(url, async ({ request }) => {
|
||||||
|
const cloned = request.clone();
|
||||||
|
captured.push({ request, body: await cloned.text() });
|
||||||
|
return response();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return captured;
|
||||||
|
};
|
||||||
|
|
||||||
describe("DeliverWebhookTask", () => {
|
describe("DeliverWebhookTask", () => {
|
||||||
test("should hit the subscription url and record a delivery", async () => {
|
test("should hit the subscription url and record a delivery", async () => {
|
||||||
const subscription = await buildWebhookSubscription({
|
const subscription = await buildWebhookSubscription({
|
||||||
@@ -25,7 +45,10 @@ describe("DeliverWebhookTask", () => {
|
|||||||
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
||||||
const processor = new DeliverWebhookTask();
|
const processor = new DeliverWebhookTask();
|
||||||
|
|
||||||
fetchMock.mockResponse("SUCCESS", { status: 200 });
|
const captured = captureWebhook(
|
||||||
|
"http://example.com",
|
||||||
|
() => new HttpResponse("SUCCESS", { status: 200 })
|
||||||
|
);
|
||||||
|
|
||||||
const event: UserEvent = {
|
const event: UserEvent = {
|
||||||
name: "users.signin",
|
name: "users.signin",
|
||||||
@@ -39,12 +62,9 @@ describe("DeliverWebhookTask", () => {
|
|||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
expect(captured.length).toBe(1);
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured[0].request.url).toBe("http://example.com/");
|
||||||
"http://example.com",
|
const parsedBody = JSON.parse(captured[0].body);
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
const parsedBody = JSON.parse(fetchMock.mock.calls[0]![1]!.body as string);
|
|
||||||
expect(parsedBody.webhookSubscriptionId).toBe(subscription.id);
|
expect(parsedBody.webhookSubscriptionId).toBe(subscription.id);
|
||||||
expect(parsedBody.event).toBe("users.signin");
|
expect(parsedBody.event).toBe("users.signin");
|
||||||
expect(parsedBody.payload.id).toBe(signedInUser.id);
|
expect(parsedBody.payload.id).toBe(signedInUser.id);
|
||||||
@@ -70,6 +90,8 @@ describe("DeliverWebhookTask", () => {
|
|||||||
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
||||||
const processor = new DeliverWebhookTask();
|
const processor = new DeliverWebhookTask();
|
||||||
|
|
||||||
|
const captured = captureWebhook("http://example.com");
|
||||||
|
|
||||||
const event: UserEvent = {
|
const event: UserEvent = {
|
||||||
name: "users.signin",
|
name: "users.signin",
|
||||||
userId: signedInUser.id,
|
userId: signedInUser.id,
|
||||||
@@ -82,13 +104,10 @@ describe("DeliverWebhookTask", () => {
|
|||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = fetchMock.mock.calls[0]![1]!.headers! as Record<
|
expect(captured.length).toBe(1);
|
||||||
string,
|
expect(captured[0].request.headers.get("Outline-Signature")).toMatch(
|
||||||
string
|
/^t=[0-9]+,s=[a-z0-9]+$/
|
||||||
>;
|
);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(headers["Outline-Signature"]).toMatch(/^t=[0-9]+,s=[a-z0-9]+$/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should hit the subscription url when the eventing model doesn't exist", async () => {
|
test("should hit the subscription url when the eventing model doesn't exist", async () => {
|
||||||
@@ -108,17 +127,16 @@ describe("DeliverWebhookTask", () => {
|
|||||||
ip,
|
ip,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const captured = captureWebhook("http://example.com");
|
||||||
|
|
||||||
await task.perform({
|
await task.perform({
|
||||||
event,
|
event,
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
expect(captured.length).toBe(1);
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(captured[0].request.url).toBe("http://example.com/");
|
||||||
"http://example.com",
|
const parsedBody = JSON.parse(captured[0].body);
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
const parsedBody = JSON.parse(fetchMock.mock.calls[0]![1]!.body as string);
|
|
||||||
expect(parsedBody.webhookSubscriptionId).toBe(subscription.id);
|
expect(parsedBody.webhookSubscriptionId).toBe(subscription.id);
|
||||||
expect(parsedBody.event).toBe("users.delete");
|
expect(parsedBody.event).toBe("users.delete");
|
||||||
expect(parsedBody.payload.id).toBe(deletedUserId);
|
expect(parsedBody.payload.id).toBe(deletedUserId);
|
||||||
@@ -140,7 +158,10 @@ describe("DeliverWebhookTask", () => {
|
|||||||
events: ["*"],
|
events: ["*"],
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.mockResponse("FAILED", { status: 500 });
|
captureWebhook(
|
||||||
|
"http://example.com",
|
||||||
|
() => new HttpResponse("FAILED", { status: 500 })
|
||||||
|
);
|
||||||
|
|
||||||
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
||||||
const task = new DeliverWebhookTask();
|
const task = new DeliverWebhookTask();
|
||||||
@@ -186,9 +207,9 @@ describe("DeliverWebhookTask", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMock.mockResponse(JSON.stringify({ message: "Failure" }), {
|
captureWebhook("http://example.com", () =>
|
||||||
status: 500,
|
HttpResponse.json({ message: "Failure" }, { status: 500 })
|
||||||
});
|
);
|
||||||
|
|
||||||
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
const signedInUser = await buildUser({ teamId: subscription.teamId });
|
||||||
const task = new DeliverWebhookTask();
|
const task = new DeliverWebhookTask();
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
|
||||||
|
|
||||||
fetchMock.enableMocks();
|
|
||||||
|
|
||||||
// changes default behavior of fetchMock to use the real 'fetch' implementation.
|
|
||||||
// Mocks can now be enabled in each individual test with fetchMock.doMock()
|
|
||||||
fetchMock.dontMock();
|
|
||||||
|
|
||||||
export default fetch;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// Mock for franc language detection library
|
|
||||||
const franc = jest.fn((text) => {
|
|
||||||
// Return 'eng' (English) by default, or 'und' (undetermined) for empty text
|
|
||||||
if (!text || text.trim().length === 0) {
|
|
||||||
return "und";
|
|
||||||
}
|
|
||||||
return "eng";
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = { franc };
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
export const franc = vi.fn((text: string) => {
|
||||||
|
if (!text || text.trim().length === 0) {
|
||||||
|
return "und";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "eng";
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// Mock for iso-639-3 language code conversion library
|
export const iso6393To1: Record<string, string | undefined> = {
|
||||||
const iso6393To1 = {
|
|
||||||
eng: "en",
|
eng: "en",
|
||||||
fra: "fr",
|
fra: "fr",
|
||||||
deu: "de",
|
deu: "de",
|
||||||
@@ -12,7 +11,5 @@ const iso6393To1 = {
|
|||||||
ara: "ar",
|
ara: "ar",
|
||||||
hin: "hi",
|
hin: "hi",
|
||||||
ben: "bn",
|
ben: "bn",
|
||||||
und: undefined, // undetermined
|
und: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { iso6393To1 };
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import http from "node:http";
|
||||||
|
import https from "node:https";
|
||||||
|
|
||||||
|
interface MockAgentOptions
|
||||||
|
extends http.AgentOptions, Pick<https.AgentOptions, "maxCachedSessions"> {}
|
||||||
|
|
||||||
|
export function useAgent(url: string, options: MockAgentOptions = {}) {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const agentOptions = {
|
||||||
|
keepAlive: options.keepAlive,
|
||||||
|
timeout: options.timeout,
|
||||||
|
keepAliveMsecs: options.keepAliveMsecs,
|
||||||
|
maxSockets: options.maxSockets,
|
||||||
|
maxFreeSockets: options.maxFreeSockets,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parsedUrl.protocol === "https:") {
|
||||||
|
return new https.Agent({
|
||||||
|
...agentOptions,
|
||||||
|
maxCachedSessions: options.maxCachedSessions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new http.Agent(agentOptions);
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@ import { sleep } from "@shared/utils/timers";
|
|||||||
import { ConnectionLimitExtension } from "./ConnectionLimitExtension";
|
import { ConnectionLimitExtension } from "./ConnectionLimitExtension";
|
||||||
import { EditorVersionExtension } from "./EditorVersionExtension";
|
import { EditorVersionExtension } from "./EditorVersionExtension";
|
||||||
|
|
||||||
jest.mock("@server/env", () => ({
|
vi.mock("@server/env", () => ({
|
||||||
|
default: {
|
||||||
|
COLLABORATION_MAX_CLIENTS_PER_DOCUMENT: 2,
|
||||||
|
},
|
||||||
COLLABORATION_MAX_CLIENTS_PER_DOCUMENT: 2,
|
COLLABORATION_MAX_CLIENTS_PER_DOCUMENT: 2,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -13,7 +16,7 @@ describe("ConnectionLimitExtension", () => {
|
|||||||
let server: typeof Server;
|
let server: typeof Server;
|
||||||
let extension: ConnectionLimitExtension;
|
let extension: ConnectionLimitExtension;
|
||||||
const port = 12345;
|
const port = 12345;
|
||||||
const url = `ws://localhost:${port}`;
|
const url = `ws://127.0.0.1:${port}`;
|
||||||
const documentName = "test";
|
const documentName = "test";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { sequelize } from "@server/storage/database";
|
|||||||
import { buildUser } from "@server/test/factories";
|
import { buildUser } from "@server/test/factories";
|
||||||
import documentImporter from "./documentImporter";
|
import documentImporter from "./documentImporter";
|
||||||
|
|
||||||
jest.mock("@server/storage/files");
|
vi.mock("@server/storage/files");
|
||||||
|
|
||||||
describe("documentImporter", () => {
|
describe("documentImporter", () => {
|
||||||
it("should convert Word Document to markdown", async () => {
|
it("should convert Word Document to markdown", async () => {
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { subDays } from "date-fns";
|
import { subDays } from "date-fns";
|
||||||
import { Attachment, Document } from "@server/models";
|
import { Attachment, Document } from "@server/models";
|
||||||
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
|
|
||||||
import { buildAttachment, buildDocument } from "@server/test/factories";
|
import { buildAttachment, buildDocument } from "@server/test/factories";
|
||||||
|
import { mockTaskSchedule } from "@server/test/support";
|
||||||
import documentPermanentDeleter from "./documentPermanentDeleter";
|
import documentPermanentDeleter from "./documentPermanentDeleter";
|
||||||
|
|
||||||
jest.mock("@server/queues/tasks/DeleteAttachmentTask");
|
const schedule = mockTaskSchedule();
|
||||||
|
|
||||||
const schedule = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
DeleteAttachmentTask.prototype.schedule = schedule;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("documentPermanentDeleter", () => {
|
describe("documentPermanentDeleter", () => {
|
||||||
it("should destroy documents", async () => {
|
it("should destroy documents", async () => {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ describe("documentUpdater", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should notify collaboration server when text changes", async () => {
|
it("should notify collaboration server when text changes", async () => {
|
||||||
const notifyUpdateSpy = jest
|
const notifyUpdateSpy = vi
|
||||||
.spyOn(APIUpdateExtension, "notifyUpdate")
|
.spyOn(APIUpdateExtension, "notifyUpdate")
|
||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ describe("documentUpdater", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not notify collaboration server when only title changes", async () => {
|
it("should not notify collaboration server when only title changes", async () => {
|
||||||
const notifyUpdateSpy = jest
|
const notifyUpdateSpy = vi
|
||||||
.spyOn(APIUpdateExtension, "notifyUpdate")
|
.spyOn(APIUpdateExtension, "notifyUpdate")
|
||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
||||||
import { requireDirectory } from "@server/utils/fs";
|
import { requireDirectory } from "@server/utils/fs";
|
||||||
|
import { createLazyRegistry } from "@server/utils/lazyRegistry";
|
||||||
import type BaseEmail from "./BaseEmail";
|
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>.
|
// 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>> = {};
|
const emails = createLazyRegistry<typeof BaseEmail<any>>(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const registry: Record<string, typeof BaseEmail<any>> = {};
|
||||||
requireDirectory<{ default: typeof BaseEmail }>(__dirname).forEach(
|
requireDirectory<{ default: typeof BaseEmail }>(__dirname).forEach(
|
||||||
([module, id]) => {
|
([module, id]) => {
|
||||||
if (id === "index") {
|
if (id === "index") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
registry[id] = module.default;
|
||||||
emails[id] = module.default;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
PluginManager.getHooks(Hook.EmailTemplate).forEach((hook) => {
|
PluginManager.getHooks(Hook.EmailTemplate).forEach((hook) => {
|
||||||
emails[hook.value.name] = hook.value;
|
registry[hook.value.name] = hook.value;
|
||||||
|
});
|
||||||
|
return registry;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default emails;
|
export default emails;
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
get: vi.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
expect(state.auth.user.id).toEqual(user.id);
|
expect(state.auth.user.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
@@ -41,12 +41,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}error`),
|
get: vi.fn(() => `Bearer ${user.getJwtToken()}error`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe("Invalid token");
|
expect(e.message).toBe("Invalid token");
|
||||||
@@ -65,12 +65,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
get: vi.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe("Invalid authentication type");
|
expect(e.message).toBe("Invalid authentication type");
|
||||||
@@ -88,12 +88,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${key.value}`),
|
get: vi.fn(() => `Bearer ${key.value}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
expect(state.auth.user.id).toEqual(user.id);
|
expect(state.auth.user.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
@@ -112,12 +112,12 @@ describe("Authentication middleware", () => {
|
|||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
url: "/auth.info",
|
url: "/auth.info",
|
||||||
get: jest.fn(() => `Bearer ${key.value}`),
|
get: vi.fn(() => `Bearer ${key.value}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
expect(state.auth.user.id).toEqual(user.id);
|
expect(state.auth.user.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
@@ -138,12 +138,12 @@ describe("Authentication middleware", () => {
|
|||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
url: "/documents.create",
|
url: "/documents.create",
|
||||||
get: jest.fn(() => `Bearer ${key.value}`),
|
get: vi.fn(() => `Bearer ${key.value}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
throw new Error("Expected error to be thrown");
|
throw new Error("Expected error to be thrown");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -162,12 +162,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${randomString(38)}`),
|
get: vi.fn(() => `Bearer ${randomString(38)}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe("Invalid API key");
|
expect(e.message).toBe("Invalid API key");
|
||||||
@@ -191,12 +191,12 @@ describe("Authentication middleware", () => {
|
|||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
url: "/users.info",
|
url: "/users.info",
|
||||||
get: jest.fn(() => `Bearer ${authentication.accessToken}`),
|
get: vi.fn(() => `Bearer ${authentication.accessToken}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
expect(state.auth.user.id).toEqual(user.id);
|
expect(state.auth.user.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
@@ -217,12 +217,12 @@ describe("Authentication middleware", () => {
|
|||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
url: "/documents.create",
|
url: "/documents.create",
|
||||||
get: jest.fn(() => `Bearer ${authentication.accessToken}`),
|
get: vi.fn(() => `Bearer ${authentication.accessToken}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toContain("does not have access to this resource");
|
expect(e.message).toContain("does not have access to this resource");
|
||||||
@@ -244,7 +244,7 @@ describe("Authentication middleware", () => {
|
|||||||
request: {
|
request: {
|
||||||
url: "/users.info",
|
url: "/users.info",
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
get: jest.fn(() => null),
|
get: vi.fn(() => null),
|
||||||
body: {
|
body: {
|
||||||
token: authentication.accessToken,
|
token: authentication.accessToken,
|
||||||
},
|
},
|
||||||
@@ -252,7 +252,7 @@ describe("Authentication middleware", () => {
|
|||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toContain(
|
expect(e.message).toContain(
|
||||||
@@ -271,12 +271,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => "error"),
|
get: vi.fn(() => "error"),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe(
|
expect(e.message).toBe(
|
||||||
@@ -293,7 +293,7 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
get: jest.fn(() => null),
|
get: vi.fn(() => null),
|
||||||
query: {
|
query: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
},
|
},
|
||||||
@@ -301,7 +301,7 @@ describe("Authentication middleware", () => {
|
|||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
expect(state.auth.user.id).toEqual(user.id);
|
expect(state.auth.user.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
@@ -314,7 +314,7 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
get: jest.fn(() => null),
|
get: vi.fn(() => null),
|
||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
},
|
},
|
||||||
@@ -322,7 +322,7 @@ describe("Authentication middleware", () => {
|
|||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
expect(state.auth.user.id).toEqual(user.id);
|
expect(state.auth.user.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
@@ -342,12 +342,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
get: vi.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
@@ -372,12 +372,12 @@ describe("Authentication middleware", () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error mock request
|
// @ts-expect-error mock request
|
||||||
request: {
|
request: {
|
||||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
get: vi.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
cache: {},
|
cache: {},
|
||||||
},
|
},
|
||||||
jest.fn()
|
vi.fn()
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe("rateLimiter middleware", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
env.RATE_LIMITER_ENABLED = originalRateLimiterEnabled;
|
env.RATE_LIMITER_ENABLED = originalRateLimiterEnabled;
|
||||||
env.RATE_LIMITER_MULTIPLIER = originalApiMultiplier;
|
env.RATE_LIMITER_MULTIPLIER = originalApiMultiplier;
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register and enforce custom rate limiter with matching paths (no mountPath)", async () => {
|
it("should register and enforce custom rate limiter with matching paths (no mountPath)", async () => {
|
||||||
@@ -29,11 +29,11 @@ describe("rateLimiter middleware", () => {
|
|||||||
path: "/documents.export",
|
path: "/documents.export",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: {},
|
request: {},
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await registerMiddleware(mockCtx, jest.fn());
|
await registerMiddleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
const registeredPath = "/documents.export";
|
const registeredPath = "/documents.export";
|
||||||
expect(RateLimiter.hasRateLimiter(registeredPath)).toBe(true);
|
expect(RateLimiter.hasRateLimiter(registeredPath)).toBe(true);
|
||||||
@@ -51,11 +51,11 @@ describe("rateLimiter middleware", () => {
|
|||||||
path: "/documents.export",
|
path: "/documents.export",
|
||||||
mountPath: "/api",
|
mountPath: "/api",
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: {},
|
request: {},
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await registerMiddleware(mockCtxRegister, jest.fn());
|
await registerMiddleware(mockCtxRegister, vi.fn());
|
||||||
|
|
||||||
const registrationPath = "/api/documents.export";
|
const registrationPath = "/api/documents.export";
|
||||||
expect(RateLimiter.hasRateLimiter(registrationPath)).toBe(true);
|
expect(RateLimiter.hasRateLimiter(registrationPath)).toBe(true);
|
||||||
@@ -73,11 +73,11 @@ describe("rateLimiter middleware", () => {
|
|||||||
path: "/documents.export",
|
path: "/documents.export",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: {},
|
request: {},
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await registerMiddleware(mockCtx, jest.fn());
|
await registerMiddleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
const limiter = RateLimiter.getRateLimiter("/documents.export");
|
const limiter = RateLimiter.getRateLimiter("/documents.export");
|
||||||
expect(limiter.points).toBe(10);
|
expect(limiter.points).toBe(10);
|
||||||
@@ -91,11 +91,11 @@ describe("rateLimiter middleware", () => {
|
|||||||
path: "/shares.subscribe",
|
path: "/shares.subscribe",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: {},
|
request: {},
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await registerMiddleware(mockCtx, jest.fn());
|
await registerMiddleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
const limiter = RateLimiter.getRateLimiter("/shares.subscribe");
|
const limiter = RateLimiter.getRateLimiter("/shares.subscribe");
|
||||||
expect(limiter.points).toBe(1);
|
expect(limiter.points).toBe(1);
|
||||||
@@ -112,16 +112,16 @@ describe("rateLimiter middleware", () => {
|
|||||||
describe("cache-keyed rate limiting", () => {
|
describe("cache-keyed rate limiting", () => {
|
||||||
it("falls back to IP when no token is present", async () => {
|
it("falls back to IP when no token is present", async () => {
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
const cacheSpy = jest.spyOn(RateLimiter, "getCachedUserIdForToken");
|
const cacheSpy = vi.spyOn(RateLimiter, "getCachedUserIdForToken");
|
||||||
|
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "192.168.1.1",
|
ip: "192.168.1.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: {
|
request: {
|
||||||
get: () => undefined,
|
get: () => undefined,
|
||||||
body: {},
|
body: {},
|
||||||
@@ -130,7 +130,7 @@ describe("rateLimiter middleware", () => {
|
|||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(cacheSpy).not.toHaveBeenCalled();
|
expect(cacheSpy).not.toHaveBeenCalled();
|
||||||
expect(consumeSpy).toHaveBeenCalledWith("192.168.1.1");
|
expect(consumeSpy).toHaveBeenCalledWith("192.168.1.1");
|
||||||
@@ -139,22 +139,22 @@ describe("rateLimiter middleware", () => {
|
|||||||
it("short-circuits to IP for API key tokens without hitting Redis or JWT verify", async () => {
|
it("short-circuits to IP for API key tokens without hitting Redis or JWT verify", async () => {
|
||||||
const apiKeyToken = `${ApiKey.prefix}${"a".repeat(38)}`;
|
const apiKeyToken = `${ApiKey.prefix}${"a".repeat(38)}`;
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
const cacheReadSpy = jest.spyOn(RateLimiter, "getCachedUserIdForToken");
|
const cacheReadSpy = vi.spyOn(RateLimiter, "getCachedUserIdForToken");
|
||||||
const verifySpy = jest.spyOn(jwtUtils, "getUserForJWT");
|
const verifySpy = vi.spyOn(jwtUtils, "getUserForJWT");
|
||||||
|
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "192.168.1.1",
|
ip: "192.168.1.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: { get: () => `Bearer ${apiKeyToken}` },
|
request: { get: () => `Bearer ${apiKeyToken}` },
|
||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(cacheReadSpy).not.toHaveBeenCalled();
|
expect(cacheReadSpy).not.toHaveBeenCalled();
|
||||||
expect(verifySpy).not.toHaveBeenCalled();
|
expect(verifySpy).not.toHaveBeenCalled();
|
||||||
@@ -163,29 +163,27 @@ describe("rateLimiter middleware", () => {
|
|||||||
|
|
||||||
it("falls back to IP when token fails verification (forged or expired)", async () => {
|
it("falls back to IP when token fails verification (forged or expired)", async () => {
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
jest
|
vi.spyOn(RateLimiter, "getCachedUserIdForToken").mockResolvedValue(null);
|
||||||
.spyOn(RateLimiter, "getCachedUserIdForToken")
|
const cacheWriteSpy = vi
|
||||||
.mockResolvedValue(null);
|
|
||||||
const cacheWriteSpy = jest
|
|
||||||
.spyOn(RateLimiter, "cacheUserForToken")
|
.spyOn(RateLimiter, "cacheUserForToken")
|
||||||
.mockResolvedValue();
|
.mockResolvedValue();
|
||||||
jest
|
vi.spyOn(jwtUtils, "getUserForJWT").mockRejectedValue(
|
||||||
.spyOn(jwtUtils, "getUserForJWT")
|
new Error("invalid token")
|
||||||
.mockRejectedValue(new Error("invalid token"));
|
);
|
||||||
|
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "192.168.1.1",
|
ip: "192.168.1.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: { get: () => "Bearer forged-or-unknown-token" },
|
request: { get: () => "Bearer forged-or-unknown-token" },
|
||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(consumeSpy).toHaveBeenCalledWith("192.168.1.1");
|
expect(consumeSpy).toHaveBeenCalledWith("192.168.1.1");
|
||||||
expect(cacheWriteSpy).not.toHaveBeenCalled();
|
expect(cacheWriteSpy).not.toHaveBeenCalled();
|
||||||
@@ -193,16 +191,14 @@ describe("rateLimiter middleware", () => {
|
|||||||
|
|
||||||
it("verifies and caches the user on cache miss, then keys by user", async () => {
|
it("verifies and caches the user on cache miss, then keys by user", async () => {
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
jest
|
vi.spyOn(RateLimiter, "getCachedUserIdForToken").mockResolvedValue(null);
|
||||||
.spyOn(RateLimiter, "getCachedUserIdForToken")
|
const cacheWriteSpy = vi
|
||||||
.mockResolvedValue(null);
|
|
||||||
const cacheWriteSpy = jest
|
|
||||||
.spyOn(RateLimiter, "cacheUserForToken")
|
.spyOn(RateLimiter, "cacheUserForToken")
|
||||||
.mockResolvedValue();
|
.mockResolvedValue();
|
||||||
jest.spyOn(jwtUtils, "getUserForJWT").mockResolvedValue({
|
vi.spyOn(jwtUtils, "getUserForJWT").mockResolvedValue({
|
||||||
user: { id: "user-abc" },
|
user: { id: "user-abc" },
|
||||||
} as never);
|
} as never);
|
||||||
|
|
||||||
@@ -210,12 +206,12 @@ describe("rateLimiter middleware", () => {
|
|||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "192.168.1.1",
|
ip: "192.168.1.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: { get: () => "Bearer valid-token" },
|
request: { get: () => "Bearer valid-token" },
|
||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(cacheWriteSpy).toHaveBeenCalledWith("valid-token", "user-abc");
|
expect(cacheWriteSpy).toHaveBeenCalledWith("valid-token", "user-abc");
|
||||||
expect(consumeSpy).toHaveBeenCalledWith("user-abc");
|
expect(consumeSpy).toHaveBeenCalledWith("user-abc");
|
||||||
@@ -223,24 +219,24 @@ describe("rateLimiter middleware", () => {
|
|||||||
|
|
||||||
it("keys on user id when token is in cache without re-verifying", async () => {
|
it("keys on user id when token is in cache without re-verifying", async () => {
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
jest
|
vi.spyOn(RateLimiter, "getCachedUserIdForToken").mockResolvedValue(
|
||||||
.spyOn(RateLimiter, "getCachedUserIdForToken")
|
"user-abc"
|
||||||
.mockResolvedValue("user-abc");
|
);
|
||||||
const verifySpy = jest.spyOn(jwtUtils, "getUserForJWT");
|
const verifySpy = vi.spyOn(jwtUtils, "getUserForJWT");
|
||||||
|
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "192.168.1.1",
|
ip: "192.168.1.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: { get: () => "Bearer verified-token" },
|
request: { get: () => "Bearer verified-token" },
|
||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(verifySpy).not.toHaveBeenCalled();
|
expect(verifySpy).not.toHaveBeenCalled();
|
||||||
expect(consumeSpy).toHaveBeenCalledWith("user-abc");
|
expect(consumeSpy).toHaveBeenCalledWith("user-abc");
|
||||||
@@ -248,23 +244,23 @@ describe("rateLimiter middleware", () => {
|
|||||||
|
|
||||||
it("falls back to IP when the cache lookup throws", async () => {
|
it("falls back to IP when the cache lookup throws", async () => {
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
.spyOn(RateLimiter.defaultRateLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
jest
|
vi.spyOn(RateLimiter, "getCachedUserIdForToken").mockRejectedValue(
|
||||||
.spyOn(RateLimiter, "getCachedUserIdForToken")
|
new Error("redis down")
|
||||||
.mockRejectedValue(new Error("redis down"));
|
);
|
||||||
|
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
mountPath: undefined,
|
mountPath: undefined,
|
||||||
ip: "192.168.1.1",
|
ip: "192.168.1.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: { get: () => "Bearer some-token" },
|
request: { get: () => "Bearer some-token" },
|
||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(consumeSpy).toHaveBeenCalledWith("192.168.1.1");
|
expect(consumeSpy).toHaveBeenCalledWith("192.168.1.1");
|
||||||
});
|
});
|
||||||
@@ -275,30 +271,30 @@ describe("rateLimiter middleware", () => {
|
|||||||
path: "/documents.export",
|
path: "/documents.export",
|
||||||
mountPath: "/api",
|
mountPath: "/api",
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: {},
|
request: {},
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
await registerMiddleware(registerCtx, jest.fn());
|
await registerMiddleware(registerCtx, vi.fn());
|
||||||
|
|
||||||
const customLimiter = RateLimiter.getRateLimiter("/api/documents.export");
|
const customLimiter = RateLimiter.getRateLimiter("/api/documents.export");
|
||||||
const consumeSpy = jest
|
const consumeSpy = vi
|
||||||
.spyOn(customLimiter, "consume")
|
.spyOn(customLimiter, "consume")
|
||||||
.mockResolvedValue({} as never);
|
.mockResolvedValue({} as never);
|
||||||
jest
|
vi.spyOn(RateLimiter, "getCachedUserIdForToken").mockResolvedValue(
|
||||||
.spyOn(RateLimiter, "getCachedUserIdForToken")
|
"user-abc"
|
||||||
.mockResolvedValue("user-abc");
|
);
|
||||||
|
|
||||||
const middleware = defaultRateLimiter();
|
const middleware = defaultRateLimiter();
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
path: "/documents.export",
|
path: "/documents.export",
|
||||||
mountPath: "/api",
|
mountPath: "/api",
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
request: { get: () => "Bearer verified-token" },
|
request: { get: () => "Bearer verified-token" },
|
||||||
cookies: { get: () => undefined },
|
cookies: { get: () => undefined },
|
||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
await middleware(mockCtx, jest.fn());
|
await middleware(mockCtx, vi.fn());
|
||||||
|
|
||||||
expect(consumeSpy).toHaveBeenCalledWith("/api/documents.export:user-abc");
|
expect(consumeSpy).toHaveBeenCalledWith("/api/documents.export:user-abc");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe("Timeout middleware", () => {
|
|||||||
const originalTimeout = 10000;
|
const originalTimeout = 10000;
|
||||||
const newTimeout = 1800000; // 30 minutes
|
const newTimeout = 1800000; // 30 minutes
|
||||||
|
|
||||||
const setTimeout = jest.fn();
|
const setTimeout = vi.fn();
|
||||||
const mockSocket = {
|
const mockSocket = {
|
||||||
timeout: originalTimeout,
|
timeout: originalTimeout,
|
||||||
setTimeout,
|
setTimeout,
|
||||||
@@ -18,7 +18,7 @@ describe("Timeout middleware", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = jest.fn();
|
const next = vi.fn();
|
||||||
const middleware = timeout(newTimeout);
|
const middleware = timeout(newTimeout);
|
||||||
|
|
||||||
await middleware(
|
await middleware(
|
||||||
@@ -39,7 +39,7 @@ describe("Timeout middleware", () => {
|
|||||||
const originalTimeout = 10000;
|
const originalTimeout = 10000;
|
||||||
const newTimeout = 1800000; // 30 minutes
|
const newTimeout = 1800000; // 30 minutes
|
||||||
|
|
||||||
const setTimeout = jest.fn();
|
const setTimeout = vi.fn();
|
||||||
const mockSocket = {
|
const mockSocket = {
|
||||||
timeout: originalTimeout,
|
timeout: originalTimeout,
|
||||||
setTimeout,
|
setTimeout,
|
||||||
@@ -52,7 +52,7 @@ describe("Timeout middleware", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const error = new Error("Test error");
|
const error = new Error("Test error");
|
||||||
const next = jest.fn().mockRejectedValue(error);
|
const next = vi.fn().mockRejectedValue(error);
|
||||||
const middleware = timeout(newTimeout);
|
const middleware = timeout(newTimeout);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -74,7 +74,7 @@ describe("Timeout middleware", () => {
|
|||||||
it("should handle undefined original timeout", async () => {
|
it("should handle undefined original timeout", async () => {
|
||||||
const newTimeout = 1800000; // 30 minutes
|
const newTimeout = 1800000; // 30 minutes
|
||||||
|
|
||||||
const setTimeout = jest.fn();
|
const setTimeout = vi.fn();
|
||||||
const mockSocket = {
|
const mockSocket = {
|
||||||
timeout: undefined,
|
timeout: undefined,
|
||||||
setTimeout,
|
setTimeout,
|
||||||
@@ -86,7 +86,7 @@ describe("Timeout middleware", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = jest.fn();
|
const next = vi.fn();
|
||||||
const middleware = timeout(newTimeout);
|
const middleware = timeout(newTimeout);
|
||||||
|
|
||||||
await middleware(
|
await middleware(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Collection from "./Collection";
|
|||||||
import Document from "./Document";
|
import Document from "./Document";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#url", () => {
|
describe("#url", () => {
|
||||||
@@ -311,7 +311,7 @@ describe("#removeDocument", () => {
|
|||||||
const document = await buildDocument({ collectionId: collection.id });
|
const document = await buildDocument({ collectionId: collection.id });
|
||||||
await collection.reload();
|
await collection.reload();
|
||||||
|
|
||||||
const saveSpy = jest.spyOn(collection, "save");
|
const saveSpy = vi.spyOn(collection, "save");
|
||||||
await collection.deleteDocument(document);
|
await collection.deleteDocument(document);
|
||||||
expect(saveSpy).toHaveBeenCalled();
|
expect(saveSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { withAPIContext } from "@server/test/support";
|
|||||||
import UserMembership from "./UserMembership";
|
import UserMembership from "./UserMembership";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#getSummary", () => {
|
describe("#getSummary", () => {
|
||||||
|
|||||||
@@ -13,7 +13,19 @@ import IdModel from "./base/IdModel";
|
|||||||
import { SkipChangeset } from "./decorators/Changeset";
|
import { SkipChangeset } from "./decorators/Changeset";
|
||||||
import Fix from "./decorators/Fix";
|
import Fix from "./decorators/Fix";
|
||||||
|
|
||||||
@Table({ tableName: "document_insights", modelName: "documentInsight" })
|
@Table({
|
||||||
|
tableName: "document_insights",
|
||||||
|
modelName: "documentInsight",
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ["documentId", "date"],
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ["teamId", "date"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
@Fix
|
@Fix
|
||||||
class DocumentInsight extends IdModel<
|
class DocumentInsight extends IdModel<
|
||||||
InferAttributes<DocumentInsight>,
|
InferAttributes<DocumentInsight>,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe("Team", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should normalize domain to lowercase", async () => {
|
it("should normalize domain to lowercase", async () => {
|
||||||
const id = randomUUID();
|
const id = randomUUID().split("-")[0];
|
||||||
const team = await buildTeam({ domain: `${id}.example.com` });
|
const team = await buildTeam({ domain: `${id}.example.com` });
|
||||||
const result = await Team.findByDomain(`${id}.Example.COM`);
|
const result = await Team.findByDomain(`${id}.Example.COM`);
|
||||||
expect(result?.id).toEqual(team.id);
|
expect(result?.id).toEqual(team.id);
|
||||||
@@ -70,21 +70,23 @@ describe("Team", () => {
|
|||||||
|
|
||||||
describe("previousSubdomains", () => {
|
describe("previousSubdomains", () => {
|
||||||
it("should list the previous subdomains", async () => {
|
it("should list the previous subdomains", async () => {
|
||||||
|
const id = randomUUID();
|
||||||
|
const originalSubdomain = `example-${id}`;
|
||||||
|
const subdomain = `updated-${id}`;
|
||||||
|
const subdomain2 = `another-${id}`;
|
||||||
const team = await buildTeam({
|
const team = await buildTeam({
|
||||||
subdomain: "example",
|
subdomain: originalSubdomain,
|
||||||
});
|
});
|
||||||
const subdomain = "updated";
|
|
||||||
|
|
||||||
await team.update({ subdomain });
|
await team.update({ subdomain });
|
||||||
expect(team.subdomain).toEqual(subdomain);
|
expect(team.subdomain).toEqual(subdomain);
|
||||||
expect(team.previousSubdomains?.length).toEqual(1);
|
expect(team.previousSubdomains?.length).toEqual(1);
|
||||||
expect(team.previousSubdomains?.[0]).toEqual("example");
|
expect(team.previousSubdomains?.[0]).toEqual(originalSubdomain);
|
||||||
|
|
||||||
const subdomain2 = "another";
|
|
||||||
await team.update({ subdomain: subdomain2 });
|
await team.update({ subdomain: subdomain2 });
|
||||||
expect(team.subdomain).toEqual(subdomain2);
|
expect(team.subdomain).toEqual(subdomain2);
|
||||||
expect(team.previousSubdomains?.length).toEqual(2);
|
expect(team.previousSubdomains?.length).toEqual(2);
|
||||||
expect(team.previousSubdomains?.[0]).toEqual("example");
|
expect(team.previousSubdomains?.[0]).toEqual(originalSubdomain);
|
||||||
expect(team.previousSubdomains?.[1]).toEqual(subdomain);
|
expect(team.previousSubdomains?.[1]).toEqual(subdomain);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -104,7 +106,8 @@ describe("Team", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return signed URL for private-bucket attachment redirect", async () => {
|
it("should return signed URL for private-bucket attachment redirect", async () => {
|
||||||
jest.useFakeTimers().setSystemTime(new Date("2026-04-16T00:00:00.000Z"));
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-04-16T00:00:00.000Z"));
|
||||||
try {
|
try {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
const attachment = await buildAttachment({
|
const attachment = await buildAttachment({
|
||||||
@@ -119,7 +122,7 @@ describe("Team", () => {
|
|||||||
const result = await team.publicAvatarUrl();
|
const result = await team.publicAvatarUrl();
|
||||||
expect(result).toEqual(await attachment.signedUrl);
|
expect(result).toEqual(await attachment.signedUrl);
|
||||||
} finally {
|
} finally {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import User from "./User";
|
|||||||
import UserMembership from "./UserMembership";
|
import UserMembership from "./UserMembership";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.useFakeTimers().setSystemTime(new Date("2018-01-02T00:00:00.000Z"));
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2018-01-02T00:00:00.000Z"));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("user model", () => {
|
describe("user model", () => {
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { DocumentHelper } from "./DocumentHelper";
|
|||||||
|
|
||||||
describe("DocumentHelper", () => {
|
describe("DocumentHelper", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
jest.setSystemTime(Date.parse("2021-01-01T00:00:00.000Z"));
|
vi.setSystemTime(Date.parse("2021-01-01T00:00:00.000Z"));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("replaceInternalUrls", () => {
|
describe("replaceInternalUrls", () => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { buildProseMirrorDoc, buildUser } from "@server/test/factories";
|
|||||||
import type { MentionAttrs } from "./ProsemirrorHelper";
|
import type { MentionAttrs } from "./ProsemirrorHelper";
|
||||||
import { ProsemirrorHelper } from "./ProsemirrorHelper";
|
import { ProsemirrorHelper } from "./ProsemirrorHelper";
|
||||||
|
|
||||||
jest.mock("@server/storage/files");
|
vi.mock("@server/storage/files");
|
||||||
|
|
||||||
describe("ProsemirrorHelper", () => {
|
describe("ProsemirrorHelper", () => {
|
||||||
describe("processMentions", () => {
|
describe("processMentions", () => {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import emojiRegex from "emoji-regex";
|
import emojiRegex from "emoji-regex";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import { chunk, isMatch } from "es-toolkit/compat";
|
import { chunk, isMatch } from "es-toolkit/compat";
|
||||||
import { EditorState } from "prosemirror-state";
|
import { EditorState, type Plugin } from "prosemirror-state";
|
||||||
import { EditorView } from "prosemirror-view";
|
import {
|
||||||
|
DecorationSet,
|
||||||
|
EditorView,
|
||||||
|
type DecorationSource,
|
||||||
|
} from "prosemirror-view";
|
||||||
import { Node, Fragment } from "prosemirror-model";
|
import { Node, Fragment } from "prosemirror-model";
|
||||||
import { renderToString } from "react-dom/server";
|
import { renderToString } from "react-dom/server";
|
||||||
import styled, { ServerStyleSheet, ThemeProvider } from "styled-components";
|
import styled, { ServerStyleSheet, ThemeProvider } from "styled-components";
|
||||||
@@ -63,6 +67,30 @@ export type MentionAttrs = {
|
|||||||
unfurl?: UnfurlResponse[keyof UnfurlResponse];
|
unfurl?: UnfurlResponse[keyof UnfurlResponse];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pluginsWithSafeDecorations = new WeakSet<Plugin>();
|
||||||
|
|
||||||
|
function isDecorationSource(value: unknown): value is DecorationSource {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("forChild" in value) || typeof value.forChild !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("members" in value && Array.isArray(value.members)) {
|
||||||
|
return value.members.every(
|
||||||
|
(member) =>
|
||||||
|
typeof member === "object" &&
|
||||||
|
member !== null &&
|
||||||
|
"localsInner" in member &&
|
||||||
|
typeof member.localsInner === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@trace()
|
@trace()
|
||||||
export class ProsemirrorHelper extends SharedProsemirrorHelper {
|
export class ProsemirrorHelper extends SharedProsemirrorHelper {
|
||||||
/**
|
/**
|
||||||
@@ -513,10 +541,39 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper {
|
|||||||
const diffPlugins = options?.changes
|
const diffPlugins = options?.changes
|
||||||
? new Diff({ changes: options.changes }).plugins
|
? new Diff({ changes: options.changes }).plugins
|
||||||
: [];
|
: [];
|
||||||
|
const editorPlugins = [...plugins, ...diffPlugins];
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (
|
||||||
|
!plugin.props.decorations ||
|
||||||
|
pluginsWithSafeDecorations.has(plugin)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.props.decorations = () => DecorationSet.empty;
|
||||||
|
pluginsWithSafeDecorations.add(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const plugin of diffPlugins) {
|
||||||
|
if (
|
||||||
|
!plugin.props.decorations ||
|
||||||
|
pluginsWithSafeDecorations.has(plugin)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decorations = plugin.props.decorations.bind(plugin);
|
||||||
|
plugin.props.decorations = (state) => {
|
||||||
|
const result = decorations(state);
|
||||||
|
return isDecorationSource(result) ? result : DecorationSet.empty;
|
||||||
|
};
|
||||||
|
pluginsWithSafeDecorations.add(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: node,
|
doc: node,
|
||||||
plugins: [...plugins, ...diffPlugins],
|
plugins: editorPlugins,
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createContext } from "@server/context";
|
|||||||
import { buildProseMirrorDoc, buildUser } from "@server/test/factories";
|
import { buildProseMirrorDoc, buildUser } from "@server/test/factories";
|
||||||
import { ProsemirrorHelper } from "./ProsemirrorHelper";
|
import { ProsemirrorHelper } from "./ProsemirrorHelper";
|
||||||
|
|
||||||
jest.mock("@server/storage/files");
|
vi.mock("@server/storage/files");
|
||||||
|
|
||||||
describe("ProsemirrorHelper", () => {
|
describe("ProsemirrorHelper", () => {
|
||||||
describe("replaceImagesWithAttachments", () => {
|
describe("replaceImagesWithAttachments", () => {
|
||||||
|
|||||||
+10
-9
@@ -1,20 +1,21 @@
|
|||||||
import type Koa from "koa";
|
import type Koa from "koa";
|
||||||
|
import type { Mock } from "vitest";
|
||||||
import { requestErrorHandler } from "@server/logging/sentry";
|
import { requestErrorHandler } from "@server/logging/sentry";
|
||||||
import { InternalError, ValidationError, NotFoundError } from "./errors";
|
import { InternalError, ValidationError, NotFoundError } from "./errors";
|
||||||
import onerror from "./onerror";
|
import onerror from "./onerror";
|
||||||
|
|
||||||
// Mock the requestErrorHandler from Sentry
|
// Mock the requestErrorHandler from Sentry
|
||||||
jest.mock("@server/logging/sentry", () => ({
|
vi.mock("@server/logging/sentry", () => ({
|
||||||
requestErrorHandler: jest.fn(),
|
requestErrorHandler: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type MockCtx = {
|
type MockCtx = {
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
headerSent: boolean;
|
headerSent: boolean;
|
||||||
writable: boolean;
|
writable: boolean;
|
||||||
accepts: jest.Mock;
|
accepts: Mock;
|
||||||
set: jest.Mock;
|
set: Mock;
|
||||||
res: { end: jest.Mock };
|
res: { end: Mock };
|
||||||
status: number | undefined;
|
status: number | undefined;
|
||||||
type: string | undefined;
|
type: string | undefined;
|
||||||
body: unknown;
|
body: unknown;
|
||||||
@@ -43,10 +44,10 @@ describe("onerror", () => {
|
|||||||
headers: {},
|
headers: {},
|
||||||
headerSent: false,
|
headerSent: false,
|
||||||
writable: true,
|
writable: true,
|
||||||
accepts: jest.fn(() => "json"),
|
accepts: vi.fn(() => "json"),
|
||||||
set: jest.fn(),
|
set: vi.fn(),
|
||||||
res: {
|
res: {
|
||||||
end: jest.fn(),
|
end: vi.fn(),
|
||||||
},
|
},
|
||||||
status: undefined,
|
status: undefined,
|
||||||
type: undefined,
|
type: undefined,
|
||||||
@@ -54,7 +55,7 @@ describe("onerror", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Clear mock calls
|
// Clear mock calls
|
||||||
(requestErrorHandler as jest.Mock).mockClear();
|
(requestErrorHandler as Mock).mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should report InternalError to Sentry", () => {
|
it("should report InternalError to Sentry", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`presents a user 1`] = `
|
exports[`presents a user 1`] = `
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe("SearchIndexProcessor", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const provider = SearchProviderManager.getProvider();
|
const provider = SearchProviderManager.getProvider();
|
||||||
const indexSpy = jest.spyOn(provider, "index");
|
const indexSpy = vi.spyOn(provider, "index");
|
||||||
|
|
||||||
await processor.perform({
|
await processor.perform({
|
||||||
name: "documents.publish",
|
name: "documents.publish",
|
||||||
@@ -63,7 +63,7 @@ describe("SearchIndexProcessor", () => {
|
|||||||
it("should call provider.remove for documents.permanent_delete", async () => {
|
it("should call provider.remove for documents.permanent_delete", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const provider = SearchProviderManager.getProvider();
|
const provider = SearchProviderManager.getProvider();
|
||||||
const removeSpy = jest.spyOn(provider, "remove");
|
const removeSpy = vi.spyOn(provider, "remove");
|
||||||
|
|
||||||
await processor.perform({
|
await processor.perform({
|
||||||
name: "documents.permanent_delete",
|
name: "documents.permanent_delete",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
||||||
import { requireDirectory } from "@server/utils/fs";
|
import { requireDirectory } from "@server/utils/fs";
|
||||||
|
import { createLazyRegistry } from "@server/utils/lazyRegistry";
|
||||||
import type BaseProcessor from "./BaseProcessor";
|
import type BaseProcessor from "./BaseProcessor";
|
||||||
|
|
||||||
const processors: Record<string, typeof BaseProcessor> = {};
|
|
||||||
|
|
||||||
const AbstractProcessors = ["ImportsProcessor"];
|
const AbstractProcessors = ["ImportsProcessor"];
|
||||||
|
|
||||||
|
const processors = createLazyRegistry(() => {
|
||||||
|
const processors: Record<string, typeof BaseProcessor> = {};
|
||||||
requireDirectory<{ default: typeof BaseProcessor }>(__dirname).forEach(
|
requireDirectory<{ default: typeof BaseProcessor }>(__dirname).forEach(
|
||||||
([module, id]) => {
|
([module, id]) => {
|
||||||
if (id === "index" || AbstractProcessors.includes(id)) {
|
if (id === "index" || AbstractProcessors.includes(id)) {
|
||||||
@@ -14,9 +15,10 @@ requireDirectory<{ default: typeof BaseProcessor }>(__dirname).forEach(
|
|||||||
processors[id] = module.default;
|
processors[id] = module.default;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
PluginManager.getHooks(Hook.Processor).forEach((hook) => {
|
PluginManager.getHooks(Hook.Processor).forEach((hook) => {
|
||||||
processors[hook.value.name] = hook.value;
|
processors[hook.value.name] = hook.value;
|
||||||
});
|
});
|
||||||
|
return processors;
|
||||||
|
});
|
||||||
|
|
||||||
export default processors;
|
export default processors;
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import DocumentPublishedNotificationsTask from "./DocumentPublishedNotifications
|
|||||||
const ip = "127.0.0.1";
|
const ip = "127.0.0.1";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("documents.publish", () => {
|
describe("documents.publish", () => {
|
||||||
test("should not send a notification to author", async () => {
|
test("should not send a notification to author", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -40,7 +40,7 @@ describe("documents.publish", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should send a notification to other users in team", async () => {
|
test("should send a notification to other users in team", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -61,7 +61,7 @@ describe("documents.publish", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should send only one notification in a 12-hour window", async () => {
|
test("should send only one notification in a 12-hour window", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -98,7 +98,7 @@ describe("documents.publish", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification to users without collection access", async () => {
|
test("should not send a notification to users without collection access", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -124,7 +124,7 @@ describe("documents.publish", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification for group mentions when disableMentions is true", async () => {
|
test("should not send a notification for group mentions when disableMentions is true", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const group = await buildGroup({
|
const group = await buildGroup({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import GroupMentionedInCommentNotificationsTask from "./GroupMentionedInCommentN
|
|||||||
const ip = "127.0.0.1";
|
const ip = "127.0.0.1";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GroupMentionedInCommentNotificationsTask", () => {
|
describe("GroupMentionedInCommentNotificationsTask", () => {
|
||||||
it("should send notifications to all group members with access", async () => {
|
it("should send notifications to all group members with access", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
@@ -91,7 +91,7 @@ describe("GroupMentionedInCommentNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send notification to actor", async () => {
|
it("should not send notification to actor", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
@@ -133,7 +133,7 @@ describe("GroupMentionedInCommentNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send notification if group has mentions disabled", async () => {
|
it("should not send notification if group has mentions disabled", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
@@ -177,7 +177,7 @@ describe("GroupMentionedInCommentNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send notification to users without subscription", async () => {
|
it("should not send notification to users without subscription", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
@@ -222,7 +222,7 @@ describe("GroupMentionedInCommentNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle large groups with batching", async () => {
|
it("should handle large groups with batching", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
@@ -269,7 +269,7 @@ describe("GroupMentionedInCommentNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send notification if document does not exist", async () => {
|
it("should not send notification if document does not exist", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function mockHandle(fileOperation: FileOperation) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
jest.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
vi.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("ImportJSONTask", () => {
|
describe("ImportJSONTask", () => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("ImportMarkdownZipTask", () => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
jest.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
vi.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
fileOperationId: fileOperation.id,
|
fileOperationId: fileOperation.id,
|
||||||
@@ -53,7 +53,7 @@ describe("ImportMarkdownZipTask", () => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
jest.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
vi.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
fileOperationId: fileOperation.id,
|
fileOperationId: fileOperation.id,
|
||||||
@@ -84,7 +84,7 @@ describe("ImportMarkdownZipTask", () => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
jest.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
vi.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
fileOperationId: fileOperation.id,
|
fileOperationId: fileOperation.id,
|
||||||
@@ -118,7 +118,7 @@ describe("ImportMarkdownZipTask", () => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
jest.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
vi.spyOn(FileOperation, "findByPk").mockResolvedValue(fileOperation);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
fileOperationId: fileOperation.id,
|
fileOperationId: fileOperation.id,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import InviteReminderTask from "./InviteReminderTask";
|
|||||||
|
|
||||||
describe("InviteReminderTask", () => {
|
describe("InviteReminderTask", () => {
|
||||||
it("should send reminder emails", async () => {
|
it("should send reminder emails", async () => {
|
||||||
const spy = jest.spyOn(InviteReminderEmail.prototype, "schedule");
|
const spy = vi.spyOn(InviteReminderEmail.prototype, "schedule");
|
||||||
|
|
||||||
// too old
|
// too old
|
||||||
await buildInvite({
|
await buildInvite({
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import RevisionCreatedNotificationsTask from "./RevisionCreatedNotificationsTask
|
|||||||
const ip = "127.0.0.1";
|
const ip = "127.0.0.1";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateDocumentText(document: Document, text: string) {
|
function updateDocumentText(document: Document, text: string) {
|
||||||
@@ -34,7 +34,7 @@ function updateDocumentText(document: Document, text: string) {
|
|||||||
|
|
||||||
describe("revisions.create", () => {
|
describe("revisions.create", () => {
|
||||||
test("should send a notification to other collaborators", async () => {
|
test("should send a notification to other collaborators", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
let document = await buildDocument({
|
let document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -64,7 +64,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification if viewed since update", async () => {
|
test("should not send a notification if viewed since update", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
let document = await buildDocument({
|
let document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -99,7 +99,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification to last editor", async () => {
|
test("should not send a notification to last editor", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
let document = await buildDocument({
|
let document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -125,7 +125,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should send a notification for subscriptions, even to collaborator", async () => {
|
test("should send a notification for subscriptions, even to collaborator", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
let document = await buildDocument({
|
let document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -225,7 +225,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send multiple emails", async () => {
|
test("should not send multiple emails", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const collaborator0 = await buildUser();
|
const collaborator0 = await buildUser();
|
||||||
const collaborator1 = await buildUser({ teamId: collaborator0.teamId });
|
const collaborator1 = await buildUser({ teamId: collaborator0.teamId });
|
||||||
const collaborator2 = await buildUser({ teamId: collaborator0.teamId });
|
const collaborator2 = await buildUser({ teamId: collaborator0.teamId });
|
||||||
@@ -265,7 +265,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not create subscriptions if previously unsubscribed", async () => {
|
test("should not create subscriptions if previously unsubscribed", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const collaborator0 = await buildUser();
|
const collaborator0 = await buildUser();
|
||||||
const collaborator1 = await buildUser({ teamId: collaborator0.teamId });
|
const collaborator1 = await buildUser({ teamId: collaborator0.teamId });
|
||||||
const collaborator2 = await buildUser({ teamId: collaborator0.teamId });
|
const collaborator2 = await buildUser({ teamId: collaborator0.teamId });
|
||||||
@@ -333,7 +333,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should send a notification for subscriptions to non-collaborators", async () => {
|
test("should send a notification for subscriptions to non-collaborators", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
let document = await buildDocument();
|
let document = await buildDocument();
|
||||||
const collaborator = await buildUser({ teamId: document.teamId });
|
const collaborator = await buildUser({ teamId: document.teamId });
|
||||||
const subscriber = await buildUser({ teamId: document.teamId });
|
const subscriber = await buildUser({ teamId: document.teamId });
|
||||||
@@ -375,7 +375,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification for subscriptions to collaborators if unsubscribed", async () => {
|
test("should not send a notification for subscriptions to collaborators if unsubscribed", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
|
|
||||||
let document = await buildDocument();
|
let document = await buildDocument();
|
||||||
const collaborator = await buildUser({ teamId: document.teamId });
|
const collaborator = await buildUser({ teamId: document.teamId });
|
||||||
@@ -421,7 +421,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification for subscriptions to members outside of the team", async () => {
|
test("should not send a notification for subscriptions to members outside of the team", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
|
|
||||||
let document = await buildDocument();
|
let document = await buildDocument();
|
||||||
const collaborator = await buildUser({ teamId: document.teamId });
|
const collaborator = await buildUser({ teamId: document.teamId });
|
||||||
@@ -470,7 +470,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification if viewed since update", async () => {
|
test("should not send a notification if viewed since update", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const collaborator = await buildUser({ teamId: document.teamId });
|
const collaborator = await buildUser({ teamId: document.teamId });
|
||||||
@@ -500,7 +500,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification to last editor", async () => {
|
test("should not send a notification to last editor", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
|
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
@@ -525,7 +525,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should send a mention notification even when change is below threshold", async () => {
|
test("should send a mention notification even when change is below threshold", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const mentioned = await buildUser({ teamId: actor.teamId, name: "Kim" });
|
const mentioned = await buildUser({ teamId: actor.teamId, name: "Kim" });
|
||||||
|
|
||||||
@@ -590,7 +590,7 @@ describe("revisions.create", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should not send a notification for group mentions when disableMentions is true", async () => {
|
test("should not send a notification for group mentions when disableMentions is true", async () => {
|
||||||
const spy = jest.spyOn(Notification, "create");
|
const spy = vi.spyOn(Notification, "create");
|
||||||
const actor = await buildUser();
|
const actor = await buildUser();
|
||||||
const group = await buildGroup({
|
const group = await buildGroup({
|
||||||
teamId: actor.teamId,
|
teamId: actor.teamId,
|
||||||
|
|||||||
@@ -16,16 +16,14 @@ const props = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
vi.setConfig({ testTimeout: 30000 });
|
||||||
|
|
||||||
const daysAgo = (n: number) => subDays(new Date(), n);
|
const daysAgo = (n: number) => subDays(new Date(), n);
|
||||||
const dayStr = (d: Date) => format(d, "yyyy-MM-dd");
|
const dayStr = (d: Date) => format(d, "yyyy-MM-dd");
|
||||||
|
|
||||||
describe("RollupDocumentInsightsTask", () => {
|
describe("RollupDocumentInsightsTask", () => {
|
||||||
let task: RollupDocumentInsightsTask;
|
let task: RollupDocumentInsightsTask;
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.setTimeout(30000);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
task = new RollupDocumentInsightsTask();
|
task = new RollupDocumentInsightsTask();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import ShareSubscriptionNotificationsTask from "./ShareSubscriptionNotifications
|
|||||||
const ip = "127.0.0.1";
|
const ip = "127.0.0.1";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ShareSubscriptionNotificationsTask", () => {
|
describe("ShareSubscriptionNotificationsTask", () => {
|
||||||
it("should send email to confirmed subscriber", async () => {
|
it("should send email to confirmed subscriber", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -43,7 +43,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send email to unconfirmed subscriber", async () => {
|
it("should not send email to unconfirmed subscriber", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -72,7 +72,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send email to unsubscribed subscriber", async () => {
|
it("should not send email to unsubscribed subscriber", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -103,7 +103,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throttle notifications to once per 6 hours", async () => {
|
it("should throttle notifications to once per 6 hours", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -134,7 +134,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should send if last notified more than 6 hours ago", async () => {
|
it("should send if last notified more than 6 hours ago", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -165,7 +165,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send for unpublished shares", async () => {
|
it("should not send for unpublished shares", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -196,7 +196,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update lastNotifiedAt after sending", async () => {
|
it("should update lastNotifiedAt after sending", async () => {
|
||||||
jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -229,7 +229,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should send to multiple subscribers", async () => {
|
it("should send to multiple subscribers", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@@ -268,7 +268,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send if document has no shares", async () => {
|
it("should not send if document has no shares", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
const document = await buildDocument();
|
const document = await buildDocument();
|
||||||
|
|
||||||
const task = new ShareSubscriptionNotificationsTask();
|
const task = new ShareSubscriptionNotificationsTask();
|
||||||
@@ -285,7 +285,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should send when child document is updated and subscription is scoped to parent", async () => {
|
it("should send when child document is updated and subscription is scoped to parent", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const parent = await buildDocument();
|
const parent = await buildDocument();
|
||||||
const child = await buildDocument({
|
const child = await buildDocument({
|
||||||
@@ -321,7 +321,7 @@ describe("ShareSubscriptionNotificationsTask", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send when updated document is outside subscription scope", async () => {
|
it("should not send when updated document is outside subscription scope", async () => {
|
||||||
const spy = jest.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
const spy = vi.spyOn(ShareDocumentUpdatedEmail.prototype, "schedule");
|
||||||
|
|
||||||
const parent = await buildDocument();
|
const parent = await buildDocument();
|
||||||
const sibling = await buildDocument({
|
const sibling = await buildDocument({
|
||||||
|
|||||||
@@ -17,32 +17,30 @@ const props = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
vi.setConfig({ testTimeout: 30000 });
|
||||||
|
|
||||||
describe("UpdateDocumentsPopularityScoreTask", () => {
|
describe("UpdateDocumentsPopularityScoreTask", () => {
|
||||||
let task: UpdateDocumentsPopularityScoreTask;
|
let task: UpdateDocumentsPopularityScoreTask;
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.setTimeout(30000);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
task = new UpdateDocumentsPopularityScoreTask();
|
task = new UpdateDocumentsPopularityScoreTask();
|
||||||
jest.spyOn(Date.prototype, "getHours").mockReturnValue(0);
|
vi.spyOn(Date.prototype, "getHours").mockReturnValue(0);
|
||||||
|
|
||||||
// Ensure calculation query sees data created in tests by redirecting to main sequelize instance.
|
// Ensure calculation query sees data created in tests by redirecting to main sequelize instance.
|
||||||
// We only mock if the instances are different to avoid infinite recursion.
|
// We only mock if the instances are different to avoid infinite recursion.
|
||||||
if (sequelizeReadOnly !== sequelize) {
|
if (sequelizeReadOnly !== sequelize) {
|
||||||
jest
|
vi.spyOn(sequelizeReadOnly, "query").mockImplementation(
|
||||||
.spyOn(sequelizeReadOnly, "query")
|
sequelize.query.bind(sequelize)
|
||||||
.mockImplementation(sequelize.query.bind(sequelize));
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should skip execution if not at a 6-hour interval", async () => {
|
it("should skip execution if not at a 6-hour interval", async () => {
|
||||||
jest.spyOn(Date.prototype, "getHours").mockReturnValue(1);
|
vi.spyOn(Date.prototype, "getHours").mockReturnValue(1);
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
||||||
import { requireDirectory } from "@server/utils/fs";
|
import { requireDirectory } from "@server/utils/fs";
|
||||||
|
import { createLazyRegistry } from "@server/utils/lazyRegistry";
|
||||||
import type { BaseTask } from "./base/BaseTask";
|
import type { BaseTask } from "./base/BaseTask";
|
||||||
|
|
||||||
|
const tasks = createLazyRegistry(() => {
|
||||||
const tasks: Record<string, typeof BaseTask> = {};
|
const tasks: Record<string, typeof BaseTask> = {};
|
||||||
|
|
||||||
requireDirectory<{ default: typeof BaseTask }>(__dirname).forEach(
|
requireDirectory<{ default: typeof BaseTask }>(__dirname).forEach(
|
||||||
([module, id]) => {
|
([module, id]) => {
|
||||||
if (id === "index") {
|
if (id === "index") {
|
||||||
@@ -12,9 +13,10 @@ requireDirectory<{ default: typeof BaseTask }>(__dirname).forEach(
|
|||||||
tasks[id] = module.default;
|
tasks[id] = module.default;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
PluginManager.getHooks(Hook.Task).forEach((hook) => {
|
PluginManager.getHooks(Hook.Task).forEach((hook) => {
|
||||||
tasks[hook.value.name] = hook.value;
|
tasks[hook.value.name] = hook.value;
|
||||||
});
|
});
|
||||||
|
return tasks;
|
||||||
|
});
|
||||||
|
|
||||||
export default tasks;
|
export default tasks;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from "@server/test/factories";
|
} from "@server/test/factories";
|
||||||
import { getTestServer } from "@server/test/support";
|
import { getTestServer } from "@server/test/support";
|
||||||
|
|
||||||
jest.mock("@server/storage/files");
|
vi.mock("@server/storage/files");
|
||||||
|
|
||||||
const server = getTestServer();
|
const server = getTestServer();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getTestServer, setSelfHosted } from "@server/test/support";
|
|||||||
|
|
||||||
const mockTeamInSessionId = randomUUID();
|
const mockTeamInSessionId = randomUUID();
|
||||||
|
|
||||||
jest.mock("@server/utils/authentication", () => ({
|
vi.mock("@server/utils/authentication", () => ({
|
||||||
getSessionsInCookie() {
|
getSessionsInCookie() {
|
||||||
return { [mockTeamInSessionId]: {} };
|
return { [mockTeamInSessionId]: {} };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#collections.add_group should require group in team 1`] = `
|
exports[`#collections.add_group > should require group in team 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -9,7 +9,7 @@ exports[`#collections.add_group should require group in team 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.add_user should not allow add self 1`] = `
|
exports[`#collections.add_user > should not allow add self 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -18,7 +18,7 @@ exports[`#collections.add_user should not allow add self 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.add_user should require user in team 1`] = `
|
exports[`#collections.add_user > should require user in team 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -27,7 +27,7 @@ exports[`#collections.add_user should require user in team 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.create should require authentication 1`] = `
|
exports[`#collections.create > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -36,7 +36,7 @@ exports[`#collections.create should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.delete should require authentication 1`] = `
|
exports[`#collections.delete > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -45,7 +45,7 @@ exports[`#collections.delete should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.export should require authentication 1`] = `
|
exports[`#collections.export > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -54,7 +54,7 @@ exports[`#collections.export should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.export_all should require authentication 1`] = `
|
exports[`#collections.export_all > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -63,7 +63,7 @@ exports[`#collections.export_all should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.group_memberships should require authentication 1`] = `
|
exports[`#collections.group_memberships > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -72,7 +72,7 @@ exports[`#collections.group_memberships should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.import should require authentication 1`] = `
|
exports[`#collections.import > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -81,7 +81,7 @@ exports[`#collections.import should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.info should require authentication 1`] = `
|
exports[`#collections.info > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -90,7 +90,7 @@ exports[`#collections.info should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.list should require authentication 1`] = `
|
exports[`#collections.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -99,7 +99,7 @@ exports[`#collections.list should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.memberships should require authentication 1`] = `
|
exports[`#collections.memberships > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -108,7 +108,7 @@ exports[`#collections.memberships should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.move should require authentication 1`] = `
|
exports[`#collections.move > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -117,7 +117,7 @@ exports[`#collections.move should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.remove_group should require group in team 1`] = `
|
exports[`#collections.remove_group > should require group in team 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -126,7 +126,7 @@ exports[`#collections.remove_group should require group in team 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.remove_user should require user in team 1`] = `
|
exports[`#collections.remove_user > should require user in team 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -135,7 +135,7 @@ exports[`#collections.remove_user should require user in team 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.update should require authentication 1`] = `
|
exports[`#collections.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#comments.add_reaction should require authentication 1`] = `
|
exports[`#comments.add_reaction > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -9,7 +9,7 @@ exports[`#comments.add_reaction should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.create should require authentication 1`] = `
|
exports[`#comments.create > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -18,7 +18,7 @@ exports[`#comments.create should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.info should require authentication 1`] = `
|
exports[`#comments.info > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -27,7 +27,7 @@ exports[`#comments.info should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.list should require authentication 1`] = `
|
exports[`#comments.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -36,7 +36,7 @@ exports[`#comments.list should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.remove_reaction should require authentication 1`] = `
|
exports[`#comments.remove_reaction > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -45,7 +45,7 @@ exports[`#comments.remove_reaction should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.resolve should require authentication 1`] = `
|
exports[`#comments.resolve > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -54,7 +54,7 @@ exports[`#comments.resolve should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.unresolve should require authentication 1`] = `
|
exports[`#comments.unresolve > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -63,7 +63,7 @@ exports[`#comments.unresolve should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#comments.update should require authentication 1`] = `
|
exports[`#comments.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#documents.create should error with invalid parentDocument 1`] = `
|
exports[`#documents.create > should error with invalid parentDocument 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -9,7 +9,7 @@ exports[`#documents.create should error with invalid parentDocument 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.delete should require authentication 1`] = `
|
exports[`#documents.delete > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -18,7 +18,7 @@ exports[`#documents.delete should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.documents should require authentication 1`] = `
|
exports[`#documents.documents > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -27,7 +27,7 @@ exports[`#documents.documents should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.documents should return 403 if user does not have access to the document 1`] = `
|
exports[`#documents.documents > should return 403 if user does not have access to the document 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -36,7 +36,7 @@ exports[`#documents.documents should return 403 if user does not have access to
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.empty_trash should not allow non-admin users 1`] = `
|
exports[`#documents.empty_trash > should not allow non-admin users 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Admin role required",
|
"message": "Admin role required",
|
||||||
@@ -45,7 +45,7 @@ exports[`#documents.empty_trash should not allow non-admin users 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.empty_trash should require authentication 1`] = `
|
exports[`#documents.empty_trash > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -54,7 +54,7 @@ exports[`#documents.empty_trash should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.list should require authentication 1`] = `
|
exports[`#documents.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -63,7 +63,7 @@ exports[`#documents.list should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.restore should require authentication 1`] = `
|
exports[`#documents.restore > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -72,7 +72,7 @@ exports[`#documents.restore should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.search should require authentication 1`] = `
|
exports[`#documents.search > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication error",
|
"message": "Authentication error",
|
||||||
@@ -81,7 +81,7 @@ exports[`#documents.search should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.update should require authentication 1`] = `
|
exports[`#documents.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -90,7 +90,7 @@ exports[`#documents.update should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.update should require text while appending 1`] = `
|
exports[`#documents.update > should require text while appending 1`] = `
|
||||||
{
|
{
|
||||||
"error": "validation_error",
|
"error": "validation_error",
|
||||||
"message": "text is required when using append, prepend, or editMode",
|
"message": "text is required when using append, prepend, or editMode",
|
||||||
@@ -99,7 +99,7 @@ exports[`#documents.update should require text while appending 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#documents.viewed should require authentication 1`] = `
|
exports[`#documents.viewed > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -3335,11 +3335,11 @@ describe("#documents.import", () => {
|
|||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest
|
vi.spyOn(FileStorage, "store").mockResolvedValue(
|
||||||
.spyOn(FileStorage, "store")
|
undefined as unknown as string
|
||||||
.mockResolvedValue(undefined as unknown as string);
|
);
|
||||||
jest.spyOn(DocumentImportTask.prototype, "schedule").mockResolvedValue({
|
vi.spyOn(DocumentImportTask.prototype, "schedule").mockResolvedValue({
|
||||||
finished: jest.fn().mockResolvedValue({ documentId: document.id }),
|
finished: vi.fn().mockResolvedValue({ documentId: document.id }),
|
||||||
} as unknown as Awaited<ReturnType<DocumentImportTask["schedule"]>>);
|
} as unknown as Awaited<ReturnType<DocumentImportTask["schedule"]>>);
|
||||||
|
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
@@ -3366,7 +3366,7 @@ describe("#documents.import", () => {
|
|||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.id).toEqual(document.id);
|
expect(body.data.id).toEqual(document.id);
|
||||||
|
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should require authentication", async () => {
|
it("should require authentication", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#events.list should require authentication 1`] = `
|
exports[`#events.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getTestServer } from "@server/test/support";
|
|||||||
|
|
||||||
const server = getTestServer();
|
const server = getTestServer();
|
||||||
|
|
||||||
jest.mock("@server/storage/files");
|
vi.mock("@server/storage/files");
|
||||||
|
|
||||||
describe("#fileOperations.info", () => {
|
describe("#fileOperations.info", () => {
|
||||||
it("should return fileOperation", async () => {
|
it("should return fileOperation", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#groups.add_user should require admin 1`] = `
|
exports[`#groups.add_user > should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -9,7 +9,7 @@ exports[`#groups.add_user should require admin 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.add_user should require user in team 1`] = `
|
exports[`#groups.add_user > should require user in team 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -18,7 +18,7 @@ exports[`#groups.add_user should require user in team 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.delete should require authentication 1`] = `
|
exports[`#groups.delete > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -27,7 +27,7 @@ exports[`#groups.delete should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.info should require authentication 1`] = `
|
exports[`#groups.info > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -36,7 +36,7 @@ exports[`#groups.info should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.list should require authentication 1`] = `
|
exports[`#groups.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -45,7 +45,7 @@ exports[`#groups.list should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.memberships should require authentication 1`] = `
|
exports[`#groups.memberships > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -54,7 +54,7 @@ exports[`#groups.memberships should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.remove_user should require admin 1`] = `
|
exports[`#groups.remove_user > should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -63,7 +63,7 @@ exports[`#groups.remove_user should require admin 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.remove_user should require user in team 1`] = `
|
exports[`#groups.remove_user > should require user in team 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -72,7 +72,7 @@ exports[`#groups.remove_user should require user in team 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.update should require authentication 1`] = `
|
exports[`#groups.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -81,7 +81,7 @@ exports[`#groups.update should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#groups.update when checking for noop updates fails with validation error when name already taken 1`] = `
|
exports[`#groups.update > when checking for noop updates > fails with validation error when name already taken 1`] = `
|
||||||
{
|
{
|
||||||
"error": "validation_error",
|
"error": "validation_error",
|
||||||
"message": "The name of this group is already in use (isUniqueNameInTeam)",
|
"message": "The name of this group is already in use (isUniqueNameInTeam)",
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`oauthAuthentications.delete should require authentication 1`] = `
|
exports[`oauthAuthentications.delete > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -9,7 +9,7 @@ exports[`oauthAuthentications.delete should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`oauthAuthentications.list should require authentication 1`] = `
|
exports[`oauthAuthentications.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`oauthClients.create should require authentication 1`] = `
|
exports[`oauthClients.create > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -9,7 +9,7 @@ exports[`oauthClients.create should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`oauthClients.delete should require authentication 1`] = `
|
exports[`oauthClients.delete > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -18,7 +18,7 @@ exports[`oauthClients.delete should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`oauthClients.info should require authentication 1`] = `
|
exports[`oauthClients.info > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -27,7 +27,7 @@ exports[`oauthClients.info should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`oauthClients.list should require authentication 1`] = `
|
exports[`oauthClients.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -36,7 +36,7 @@ exports[`oauthClients.list should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`oauthClients.rotate_secret should require authentication 1`] = `
|
exports[`oauthClients.rotate_secret > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -45,7 +45,7 @@ exports[`oauthClients.rotate_secret should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`oauthclients.update should require authentication 1`] = `
|
exports[`oauthclients.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#reactions.list should require authentication 1`] = `
|
exports[`#reactions.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#shares.create should require authentication 1`] = `
|
exports[`#shares.create > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -9,7 +9,7 @@ exports[`#shares.create should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#shares.list should require authentication 1`] = `
|
exports[`#shares.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -18,7 +18,7 @@ exports[`#shares.list should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#shares.revoke should require authentication 1`] = `
|
exports[`#shares.revoke > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -27,7 +27,7 @@ exports[`#shares.revoke should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#shares.update should require authentication 1`] = `
|
exports[`#shares.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Mock } from "vitest";
|
||||||
import { UnfurlResourceType } from "@shared/types";
|
import { UnfurlResourceType } from "@shared/types";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import type { User } from "@server/models";
|
import type { User } from "@server/models";
|
||||||
@@ -10,22 +11,38 @@ import {
|
|||||||
import { getTestServer } from "@server/test/support";
|
import { getTestServer } from "@server/test/support";
|
||||||
import Iframely from "plugins/iframely/server/iframely";
|
import Iframely from "plugins/iframely/server/iframely";
|
||||||
|
|
||||||
jest.mock("dns", () => ({
|
const resolveCname = vi.hoisted(
|
||||||
resolveCname: (
|
() =>
|
||||||
|
(
|
||||||
input: string,
|
input: string,
|
||||||
callback: (err: Error | null, addresses: string[]) => void
|
callback: (err: Error | null, addresses: string[]) => void
|
||||||
) => {
|
) => {
|
||||||
if (input.includes("valid.custom.domain")) {
|
if (input.includes("valid.custom.domain")) {
|
||||||
callback(null, ["secure.outline.dev"]);
|
callback(null, ["secure.outline.dev"]);
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
callback(null, []);
|
callback(null, []);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock("node:dns", () => ({
|
||||||
|
default: {
|
||||||
|
resolveCname,
|
||||||
},
|
},
|
||||||
|
resolveCname,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest
|
vi.mock("dns", () => ({
|
||||||
.spyOn(Iframely, "requestResource")
|
default: {
|
||||||
.mockImplementation(() => Promise.resolve({}));
|
resolveCname,
|
||||||
|
},
|
||||||
|
resolveCname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.spyOn(Iframely, "requestResource").mockImplementation(() =>
|
||||||
|
Promise.resolve({})
|
||||||
|
);
|
||||||
|
|
||||||
const server = getTestServer();
|
const server = getTestServer();
|
||||||
|
|
||||||
@@ -287,7 +304,7 @@ describe("#urls.unfurl", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should succeed with status 200 ok for a valid external url", async () => {
|
it("should succeed with status 200 ok for a valid external url", async () => {
|
||||||
(Iframely.requestResource as jest.Mock).mockResolvedValue(
|
(Iframely.requestResource as Mock).mockResolvedValue(
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
url: "https://www.flickr.com",
|
url: "https://www.flickr.com",
|
||||||
type: "rich",
|
type: "rich",
|
||||||
@@ -343,7 +360,7 @@ describe("#urls.unfurl", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should succeed with status 204 no content for a non-existing external url", async () => {
|
it("should succeed with status 204 no content for a non-existing external url", async () => {
|
||||||
(Iframely.requestResource as jest.Mock).mockResolvedValue(
|
(Iframely.requestResource as Mock).mockResolvedValue(
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
status: 404,
|
status: 404,
|
||||||
error:
|
error:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#users.activate should require admin 1`] = `
|
exports[`#users.activate > should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -9,7 +9,7 @@ exports[`#users.activate should require admin 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.delete should require authentication 1`] = `
|
exports[`#users.delete > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -18,7 +18,7 @@ exports[`#users.delete should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.demote should not allow demoting self 1`] = `
|
exports[`#users.demote > should not allow demoting self 1`] = `
|
||||||
{
|
{
|
||||||
"error": "validation_error",
|
"error": "validation_error",
|
||||||
"message": "You cannot change your own role",
|
"message": "You cannot change your own role",
|
||||||
@@ -27,7 +27,7 @@ exports[`#users.demote should not allow demoting self 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.demote should require admin 1`] = `
|
exports[`#users.demote > should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Admin role required",
|
"message": "Admin role required",
|
||||||
@@ -36,7 +36,7 @@ exports[`#users.demote should require admin 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.promote should require admin 1`] = `
|
exports[`#users.promote > should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Admin role required",
|
"message": "Admin role required",
|
||||||
@@ -45,7 +45,7 @@ exports[`#users.promote should require admin 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.suspend should not allow suspending self 1`] = `
|
exports[`#users.suspend > should not allow suspending self 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -54,7 +54,7 @@ exports[`#users.suspend should not allow suspending self 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.suspend should require admin 1`] = `
|
exports[`#users.suspend > should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authorization_error",
|
"error": "authorization_error",
|
||||||
"message": "Authorization error",
|
"message": "Authorization error",
|
||||||
@@ -63,7 +63,7 @@ exports[`#users.suspend should require admin 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.update should require authentication 1`] = `
|
exports[`#users.update > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -72,7 +72,7 @@ exports[`#users.update should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.updateEmail post should fail if email not in allowed domains 1`] = `
|
exports[`#users.updateEmail > post > should fail if email not in allowed domains 1`] = `
|
||||||
{
|
{
|
||||||
"error": "validation_error",
|
"error": "validation_error",
|
||||||
"message": "The domain is not allowed for this workspace",
|
"message": "The domain is not allowed for this workspace",
|
||||||
@@ -81,7 +81,7 @@ exports[`#users.updateEmail post should fail if email not in allowed domains 1`]
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.updateEmail post should fail if email not unique in workspace 1`] = `
|
exports[`#users.updateEmail > post > should fail if email not unique in workspace 1`] = `
|
||||||
{
|
{
|
||||||
"error": "validation_error",
|
"error": "validation_error",
|
||||||
"message": "User with email already exists",
|
"message": "User with email already exists",
|
||||||
@@ -90,7 +90,7 @@ exports[`#users.updateEmail post should fail if email not unique in workspace 1`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#users.updateEmail post should require authentication 1`] = `
|
exports[`#users.updateEmail > post > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import { getTestServer } from "@server/test/support";
|
|||||||
const server = getTestServer();
|
const server = getTestServer();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.useFakeTimers().setSystemTime(new Date("2018-01-02T00:00:00.000Z"));
|
vi.useFakeTimers().setSystemTime(new Date("2018-01-02T00:00:00.000Z"));
|
||||||
});
|
});
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#users.list", () => {
|
describe("#users.list", () => {
|
||||||
@@ -766,7 +766,7 @@ describe("#users.update", () => {
|
|||||||
describe("#users.updateEmail", () => {
|
describe("#users.updateEmail", () => {
|
||||||
describe("post", () => {
|
describe("post", () => {
|
||||||
it("should trigger verification email", async () => {
|
it("should trigger verification email", async () => {
|
||||||
const spy = jest.spyOn(ConfirmUpdateEmail.prototype, "schedule");
|
const spy = vi.spyOn(ConfirmUpdateEmail.prototype, "schedule");
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const res = await server.post("/api/users.updateEmail", {
|
const res = await server.post("/api/users.updateEmail", {
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`#views.create should require authentication 1`] = `
|
exports[`#views.create > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
@@ -9,7 +9,7 @@ exports[`#views.create should require authentication 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#views.list should require authentication 1`] = `
|
exports[`#views.list > should require authentication 1`] = `
|
||||||
{
|
{
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
|||||||
@@ -233,10 +233,6 @@ export function createMigrationRunner(
|
|||||||
* See https://github.com/sequelize/sequelize/issues/14807#issuecomment-1854398131
|
* See https://github.com/sequelize/sequelize/issues/14807#issuecomment-1854398131
|
||||||
*/
|
*/
|
||||||
export function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
|
export function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
|
||||||
if (typeof jest === "undefined") {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sequelizeVersion = (Sequelize as unknown as { version: string })
|
const sequelizeVersion = (Sequelize as unknown as { version: string })
|
||||||
.version;
|
.version;
|
||||||
const major = sequelizeVersion.split(".").map(Number)[0];
|
const major = sequelizeVersion.split(".").map(Number)[0];
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
upload: jest.fn().mockReturnValue("/endpoint/key"),
|
upload: vi.fn().mockReturnValue("/endpoint/key"),
|
||||||
|
|
||||||
getUploadUrl: jest.fn().mockReturnValue("http://mock/create"),
|
getUploadUrl: vi.fn().mockReturnValue("http://mock/create"),
|
||||||
|
|
||||||
getUrlForKey: jest.fn().mockReturnValue("http://mock/get"),
|
getUrlForKey: vi.fn().mockReturnValue("http://mock/get"),
|
||||||
|
|
||||||
getSignedUrl: jest.fn().mockReturnValue("http://s3mock"),
|
getSignedUrl: vi.fn().mockReturnValue("http://s3mock"),
|
||||||
|
|
||||||
getPresignedPost: jest.fn().mockReturnValue({}),
|
getPresignedPost: vi.fn().mockReturnValue({}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { sequelize } from "@server/storage/database";
|
|
||||||
|
|
||||||
module.exports = async function (opts) {
|
|
||||||
if (!opts.watch && !opts.watchAll) {
|
|
||||||
await sequelize.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default function setup() {
|
||||||
|
return async () => {
|
||||||
|
const { sequelize } = await import("@server/storage/database");
|
||||||
|
await sequelize.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { http, passthrough } from "msw";
|
||||||
|
import { setupServer } from "msw/node";
|
||||||
|
|
||||||
|
// Pass-through handlers for in-process supertest requests. Registered as
|
||||||
|
// initial handlers so they survive server.resetHandlers() between tests.
|
||||||
|
const passthroughLocalhost = http.all(
|
||||||
|
/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?(\/|$)/,
|
||||||
|
() => passthrough()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const server = setupServer(passthroughLocalhost);
|
||||||
+37
-32
@@ -1,55 +1,60 @@
|
|||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
|
||||||
import sharedEnv from "@shared/env";
|
import sharedEnv from "@shared/env";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import { EventEmitter } from "node:events";
|
import { server } from "./msw";
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
// Increase the default max listeners for EventEmitter to prevent warnings in tests
|
// Increase the default max listeners for EventEmitter to prevent warnings in tests
|
||||||
// This needs to be done before any modules that use EventEmitter are loaded
|
// This needs to be done before any modules that use EventEmitter are loaded
|
||||||
EventEmitter.defaultMaxListeners = 100;
|
EventEmitter.defaultMaxListeners = 100;
|
||||||
|
|
||||||
// Save native Response/Request before jest-fetch-mock replaces them with
|
|
||||||
// cross-fetch polyfills that don't support Web Streams (e.g. ReadableStream
|
|
||||||
// bodies lose their getReader method). The MCP SDK's @hono/node-server adapter
|
|
||||||
// depends on proper Web Streams support.
|
|
||||||
const NativeResponse = globalThis.Response;
|
|
||||||
const NativeRequest = globalThis.Request;
|
|
||||||
const NativeHeaders = globalThis.Headers;
|
|
||||||
|
|
||||||
// Enable fetch mocks for testing
|
|
||||||
require("jest-fetch-mock").enableMocks();
|
|
||||||
fetchMock.dontMock();
|
|
||||||
|
|
||||||
// Restore native Web API classes
|
|
||||||
globalThis.Response = NativeResponse;
|
|
||||||
globalThis.Request = NativeRequest;
|
|
||||||
globalThis.Headers = NativeHeaders;
|
|
||||||
|
|
||||||
// Mock AWS SDK S3 client and related commands
|
// Mock AWS SDK S3 client and related commands
|
||||||
jest.mock("@aws-sdk/client-s3", () => ({
|
vi.mock("@aws-sdk/client-s3", () => ({
|
||||||
S3Client: jest.fn(() => ({
|
S3Client: vi.fn(() => ({
|
||||||
send: jest.fn(),
|
send: vi.fn(),
|
||||||
})),
|
})),
|
||||||
DeleteObjectCommand: jest.fn(),
|
DeleteObjectCommand: vi.fn(),
|
||||||
GetObjectCommand: jest.fn(),
|
GetObjectCommand: vi.fn(),
|
||||||
ObjectCannedACL: {},
|
ObjectCannedACL: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@aws-sdk/lib-storage", () => ({
|
vi.mock("@aws-sdk/lib-storage", () => ({
|
||||||
Upload: jest.fn(() => ({
|
Upload: vi.fn(() => ({
|
||||||
done: jest.fn(),
|
done: vi.fn(),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@aws-sdk/s3-presigned-post", () => ({
|
vi.mock("@aws-sdk/s3-presigned-post", () => ({
|
||||||
createPresignedPost: jest.fn(),
|
createPresignedPost: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@aws-sdk/s3-request-presigner", () => ({
|
vi.mock("@aws-sdk/s3-request-presigner", () => ({
|
||||||
getSignedUrl: jest.fn(),
|
getSignedUrl: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Initialize the database models
|
// Initialize the database models. Loaded dynamically so the
|
||||||
require("@server/storage/database");
|
// EventEmitter.defaultMaxListeners assignment above runs first; static imports
|
||||||
|
// would be hoisted ahead of it.
|
||||||
|
await import("@server/storage/database");
|
||||||
|
|
||||||
|
// Eagerly load plugin server entry points so that PluginManager.getHooks()
|
||||||
|
// returns the registered plugins. Vitest does not support require() of TS
|
||||||
|
// files with bare imports (e.g. `@server/...`), so we use Vite's
|
||||||
|
// import.meta.glob to load them through the Vite resolver instead.
|
||||||
|
const { PluginManager } = await import("@server/utils/PluginManager");
|
||||||
|
const pluginModules = import.meta.glob(
|
||||||
|
"../../plugins/*/server/!(*.test|schema).{js,ts}",
|
||||||
|
{ eager: true }
|
||||||
|
);
|
||||||
|
void pluginModules;
|
||||||
|
// Mark as loaded so PluginManager.loadPlugins() (which uses require()) is a
|
||||||
|
// no-op during tests.
|
||||||
|
(PluginManager as unknown as { loaded: boolean }).loaded = true;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
env.URL = sharedEnv.URL = "https://app.outline.dev";
|
env.URL = sharedEnv.URL = "https://app.outline.dev";
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// This file runs before the test environment is set up to ensure mocks are registered early
|
|
||||||
// It prevents real Redis clients from being initialized during module imports
|
|
||||||
|
|
||||||
// Mock ioredis with ioredis-mock before any imports
|
|
||||||
jest.mock("ioredis", () => require("ioredis-mock"));
|
|
||||||
|
|
||||||
// Mock other Redis-dependent modules
|
|
||||||
jest.mock("@server/utils/MutexLock");
|
|
||||||
|
|
||||||
// Mock AWS SDK signature module to prevent aws_logger open handle
|
|
||||||
jest.mock("@aws-sdk/signature-v4-crt", () => ({}));
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// This file runs before the test environment is set up to ensure mocks are registered early.
|
||||||
|
// It prevents real Redis clients from being initialized during module imports.
|
||||||
|
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import type * as IORedisMock from "ioredis-mock";
|
||||||
|
import { __setRequireDirectoryCache } from "@server/utils/fs";
|
||||||
|
|
||||||
|
// Pre-populate the requireDirectory cache used by @server/utils/fs so that
|
||||||
|
// tasks/processors/email-templates can be looked up via their pre-loaded
|
||||||
|
// modules instead of via Node's require(), which cannot resolve TypeScript
|
||||||
|
// files with aliased imports under Vitest. The eager globs intentionally
|
||||||
|
// exclude index.ts files (which call requireDirectory themselves and would
|
||||||
|
// recurse) and any files whose imports would themselves load the directory
|
||||||
|
// they live in.
|
||||||
|
__setRequireDirectoryCache(
|
||||||
|
"emails/templates",
|
||||||
|
import.meta.glob("../emails/templates/!(index|*.test).{js,ts}", {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
__setRequireDirectoryCache(
|
||||||
|
"queues/processors",
|
||||||
|
import.meta.glob("../queues/processors/!(index|*.test).{js,ts}", {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
__setRequireDirectoryCache(
|
||||||
|
"queues/tasks",
|
||||||
|
import.meta.glob("../queues/tasks/!(index|*.test).{js,ts}", {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock("ioredis", async () => {
|
||||||
|
const mod = await vi.importActual<typeof IORedisMock>("ioredis-mock");
|
||||||
|
return mod;
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@server/utils/MutexLock");
|
||||||
|
|
||||||
|
vi.mock("@aws-sdk/signature-v4-crt", () => ({}));
|
||||||
|
|
||||||
|
// Auto-mock these modules using the corresponding files under server/__mocks__/.
|
||||||
|
// In Jest, __mocks__ next to a `roots` directory is auto-applied; vitest
|
||||||
|
// requires an explicit vi.mock() call to wire them up.
|
||||||
|
vi.mock("bull", () => import("../__mocks__/bull"));
|
||||||
|
vi.mock("dd-trace", async () => {
|
||||||
|
const mod = await import("../__mocks__/dd-trace");
|
||||||
|
return { default: mod.mockTracer, ...mod };
|
||||||
|
});
|
||||||
|
vi.mock("franc", () => import("../__mocks__/franc"));
|
||||||
|
vi.mock("iso-639-3", () => import("../__mocks__/iso-639-3"));
|
||||||
|
vi.mock(
|
||||||
|
"request-filtering-agent",
|
||||||
|
() => import("../__mocks__/request-filtering-agent")
|
||||||
|
);
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
import sharedEnv from "@shared/env";
|
import sharedEnv from "@shared/env";
|
||||||
import { createContext } from "@server/context";
|
import { createContext } from "@server/context";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import type { User } from "@server/models";
|
import type { User } from "@server/models";
|
||||||
import onerror from "@server/onerror";
|
import onerror from "@server/onerror";
|
||||||
|
import { BaseTask } from "@server/queues/tasks/base/BaseTask";
|
||||||
import webService from "@server/services/web";
|
import webService from "@server/services/web";
|
||||||
import { sequelize } from "@server/storage/database";
|
import { sequelize } from "@server/storage/database";
|
||||||
import type { APIContext } from "@server/types";
|
import type { APIContext } from "@server/types";
|
||||||
@@ -33,6 +35,26 @@ export function setSelfHosted() {
|
|||||||
env.URL = sharedEnv.URL = `https://${faker.internet.domainName()}`;
|
env.URL = sharedEnv.URL = `https://${faker.internet.domainName()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock scheduling for all task subclasses in the current test file.
|
||||||
|
*
|
||||||
|
* @returns the schedule mock for assertions.
|
||||||
|
*/
|
||||||
|
export function mockTaskSchedule() {
|
||||||
|
const schedule = vi.fn<BaseTask<object>["schedule"]>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
schedule.mockReset();
|
||||||
|
vi.spyOn(BaseTask.prototype, "schedule").mockImplementation(schedule);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
}
|
||||||
|
|
||||||
export function withAPIContext<T>(
|
export function withAPIContext<T>(
|
||||||
user: User,
|
user: User,
|
||||||
fn: (ctx: APIContext) => T
|
fn: (ctx: APIContext) => T
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
export class MutexLock {
|
export class MutexLock {
|
||||||
// Default expiry time for acquiring lock in milliseconds
|
// Default expiry time for acquiring lock in milliseconds
|
||||||
public static defaultLockTimeout = 4000;
|
public static defaultLockTimeout = 4000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquires a mock lock.
|
||||||
|
*/
|
||||||
|
public static acquire = vi.fn().mockResolvedValue({
|
||||||
|
release: vi.fn().mockResolvedValue(true),
|
||||||
|
expiration: Date.now() + 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases a mock lock.
|
||||||
|
*/
|
||||||
|
public static release = vi.fn().mockResolvedValue(true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the mock redlock instance
|
* Returns the mock redlock instance
|
||||||
*/
|
*/
|
||||||
public static get lock() {
|
public static get lock() {
|
||||||
return {
|
return {
|
||||||
acquire: jest.fn().mockResolvedValue({
|
acquire: vi.fn().mockResolvedValue({
|
||||||
release: jest.fn().mockResolvedValue(true),
|
release: vi.fn().mockResolvedValue(true),
|
||||||
expiration: Date.now() + 10000,
|
expiration: Date.now() + 10000,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
+51
-54
@@ -1,9 +1,24 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import { http, HttpResponse } from "msw";
|
||||||
|
import { server } from "@server/test/msw";
|
||||||
import { checkEmbeddability, convertBareUrlsToEmbedMarkdown } from "./embeds";
|
import { checkEmbeddability, convertBareUrlsToEmbedMarkdown } from "./embeds";
|
||||||
|
|
||||||
beforeEach(() => {
|
const embedUrl = "https://www.example.com/embed";
|
||||||
fetchMock.resetMocks();
|
|
||||||
});
|
const mockEmbedResponse = (
|
||||||
|
url: string,
|
||||||
|
init: { status?: number; headers?: Record<string, string> } = {}
|
||||||
|
) => {
|
||||||
|
server.use(
|
||||||
|
http.get(
|
||||||
|
url,
|
||||||
|
() =>
|
||||||
|
new HttpResponse(null, {
|
||||||
|
status: init.status ?? 200,
|
||||||
|
headers: init.headers ?? {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
describe("checkEmbeddability", () => {
|
describe("checkEmbeddability", () => {
|
||||||
describe("when URL doesn't match any embed pattern", () => {
|
describe("when URL doesn't match any embed pattern", () => {
|
||||||
@@ -21,55 +36,48 @@ describe("checkEmbeddability", () => {
|
|||||||
|
|
||||||
describe("when URL matches an embed pattern", () => {
|
describe("when URL matches an embed pattern", () => {
|
||||||
it("should return embeddable: true when no restrictive headers", async () => {
|
it("should return embeddable: true when no restrictive headers", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl);
|
||||||
status: 200,
|
|
||||||
headers: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: true });
|
expect(result).toEqual({ embeddable: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when X-Frame-Options: DENY", async () => {
|
it("should return embeddable: false when X-Frame-Options: DENY", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: { "X-Frame-Options": "DENY" },
|
headers: { "X-Frame-Options": "DENY" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: false, reason: "x-frame-options" });
|
expect(result).toEqual({ embeddable: false, reason: "x-frame-options" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when X-Frame-Options: SAMEORIGIN", async () => {
|
it("should return embeddable: false when X-Frame-Options: SAMEORIGIN", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: { "X-Frame-Options": "SAMEORIGIN" },
|
headers: { "X-Frame-Options": "SAMEORIGIN" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: false, reason: "x-frame-options" });
|
expect(result).toEqual({ embeddable: false, reason: "x-frame-options" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when X-Frame-Options: ALLOW-FROM", async () => {
|
it("should return embeddable: false when X-Frame-Options: ALLOW-FROM", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: { "X-Frame-Options": "ALLOW-FROM https://example.com" },
|
headers: { "X-Frame-Options": "ALLOW-FROM https://example.com" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: false, reason: "x-frame-options" });
|
expect(result).toEqual({ embeddable: false, reason: "x-frame-options" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when CSP frame-ancestors is 'none'", async () => {
|
it("should return embeddable: false when CSP frame-ancestors is 'none'", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Security-Policy":
|
"Content-Security-Policy":
|
||||||
"default-src 'self'; frame-ancestors 'none'",
|
"default-src 'self'; frame-ancestors 'none'",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
embeddable: false,
|
embeddable: false,
|
||||||
reason: "csp-frame-ancestors",
|
reason: "csp-frame-ancestors",
|
||||||
@@ -77,14 +85,13 @@ describe("checkEmbeddability", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when CSP frame-ancestors is 'self'", async () => {
|
it("should return embeddable: false when CSP frame-ancestors is 'self'", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Security-Policy": "frame-ancestors 'self'",
|
"Content-Security-Policy": "frame-ancestors 'self'",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
embeddable: false,
|
embeddable: false,
|
||||||
reason: "csp-frame-ancestors",
|
reason: "csp-frame-ancestors",
|
||||||
@@ -92,26 +99,24 @@ describe("checkEmbeddability", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: true when CSP frame-ancestors is *", async () => {
|
it("should return embeddable: true when CSP frame-ancestors is *", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Security-Policy": "frame-ancestors *",
|
"Content-Security-Policy": "frame-ancestors *",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: true });
|
expect(result).toEqual({ embeddable: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when CSP frame-ancestors has specific origins", async () => {
|
it("should return embeddable: false when CSP frame-ancestors has specific origins", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Security-Policy": "frame-ancestors https://allowed-site.com",
|
"Content-Security-Policy": "frame-ancestors https://allowed-site.com",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
embeddable: false,
|
embeddable: false,
|
||||||
reason: "csp-frame-ancestors",
|
reason: "csp-frame-ancestors",
|
||||||
@@ -119,60 +124,52 @@ describe("checkEmbeddability", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when COEP is require-corp", async () => {
|
it("should return embeddable: false when COEP is require-corp", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: { "Cross-Origin-Embedder-Policy": "require-corp" },
|
headers: { "Cross-Origin-Embedder-Policy": "require-corp" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: false, reason: "coep" });
|
expect(result).toEqual({ embeddable: false, reason: "coep" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: true when COEP is unsafe-none", async () => {
|
it("should return embeddable: true when COEP is unsafe-none", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
mockEmbedResponse(embedUrl, {
|
||||||
status: 200,
|
|
||||||
headers: { "Cross-Origin-Embedder-Policy": "unsafe-none" },
|
headers: { "Cross-Origin-Embedder-Policy": "unsafe-none" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: true });
|
expect(result).toEqual({ embeddable: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when server returns 403", async () => {
|
it("should return embeddable: false when server returns 403", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
const url = "https://www.example.com/forbiddenpage";
|
||||||
status: 403,
|
mockEmbedResponse(url, { status: 403 });
|
||||||
headers: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await checkEmbeddability(
|
const result = await checkEmbeddability(url);
|
||||||
"https://www.example.com/forbiddenpage"
|
|
||||||
);
|
|
||||||
expect(result).toEqual({ embeddable: false, reason: "http-error" });
|
expect(result).toEqual({ embeddable: false, reason: "http-error" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: false when server returns 404", async () => {
|
it("should return embeddable: false when server returns 404", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
const url = "https://www.example.com/nonexistentpage";
|
||||||
status: 404,
|
mockEmbedResponse(url, { status: 404 });
|
||||||
headers: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await checkEmbeddability(
|
const result = await checkEmbeddability(url);
|
||||||
"https://www.example.com/nonexistentpage"
|
|
||||||
);
|
|
||||||
expect(result).toEqual({ embeddable: false, reason: "http-error" });
|
expect(result).toEqual({ embeddable: false, reason: "http-error" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: true on timeout (optimistic)", async () => {
|
it("should return embeddable: true on timeout (optimistic)", async () => {
|
||||||
fetchMock.mockAbortOnce();
|
// Network errors and aborts both land in the catch branch and return
|
||||||
|
// { embeddable: true, reason: "timeout" }.
|
||||||
|
server.use(http.get(embedUrl, () => HttpResponse.error()));
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: true, reason: "timeout" });
|
expect(result).toEqual({ embeddable: true, reason: "timeout" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return embeddable: true on network error (optimistic)", async () => {
|
it("should return embeddable: true on network error (optimistic)", async () => {
|
||||||
fetchMock.mockRejectOnce(new Error("Network error"));
|
server.use(http.get(embedUrl, () => HttpResponse.error()));
|
||||||
|
|
||||||
const result = await checkEmbeddability("https://www.example.com/embed");
|
const result = await checkEmbeddability(embedUrl);
|
||||||
expect(result).toEqual({ embeddable: true, reason: "timeout" });
|
expect(result).toEqual({ embeddable: true, reason: "timeout" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,6 +112,25 @@ export function getFilenamesInDirectory(dirName: string): string[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional cache used in tests, where Node's require() cannot resolve
|
||||||
|
// TypeScript files with aliased imports. Populated by the test setup with
|
||||||
|
// modules pre-loaded via Vite's import.meta.glob, keyed by directory suffix.
|
||||||
|
const requireDirectoryCache = new Map<string, Record<string, unknown>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-populate requireDirectory's module cache. Intended for use only by the
|
||||||
|
* Vitest test setup; production code should not call this.
|
||||||
|
*
|
||||||
|
* @param suffix The directory path suffix to match against.
|
||||||
|
* @param modules The eagerly-loaded modules.
|
||||||
|
*/
|
||||||
|
export function __setRequireDirectoryCache(
|
||||||
|
suffix: string,
|
||||||
|
modules: Record<string, unknown>
|
||||||
|
): void {
|
||||||
|
requireDirectoryCache.set(suffix, modules);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Require all files in a directory and return them as an array of tuples.
|
* Require all files in a directory and return them as an array of tuples.
|
||||||
*
|
*
|
||||||
@@ -119,6 +138,22 @@ export function getFilenamesInDirectory(dirName: string): string[] {
|
|||||||
* @returns An array of tuples containing the required files and their names.
|
* @returns An array of tuples containing the required files and their names.
|
||||||
*/
|
*/
|
||||||
export function requireDirectory<T>(dirName: string): [T, string][] {
|
export function requireDirectory<T>(dirName: string): [T, string][] {
|
||||||
|
for (const [suffix, modules] of requireDirectoryCache) {
|
||||||
|
if (dirName.endsWith(suffix)) {
|
||||||
|
return Object.entries(modules)
|
||||||
|
.filter(
|
||||||
|
([filePath]) =>
|
||||||
|
!filePath.endsWith("/index.ts") &&
|
||||||
|
!filePath.endsWith("/index.js") &&
|
||||||
|
!filePath.includes(".test.")
|
||||||
|
)
|
||||||
|
.map(([filePath, mod]) => {
|
||||||
|
const base = filePath.split("/").pop() ?? filePath;
|
||||||
|
const id = base.replace(/\.[jt]s$/, "");
|
||||||
|
return [mod as T, id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return getFilenamesInDirectory(dirName).map((fileName) => {
|
return getFilenamesInDirectory(dirName).map((fileName) => {
|
||||||
const filePath = path.join(dirName, fileName);
|
const filePath = path.join(dirName, fileName);
|
||||||
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
|
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import { http, HttpResponse } from "msw";
|
||||||
|
import { server } from "@server/test/msw";
|
||||||
import { getVersionInfo, getVersion } from "./getInstallationInfo";
|
import { getVersionInfo, getVersion } from "./getInstallationInfo";
|
||||||
|
|
||||||
beforeEach(() => {
|
const dockerHubUrl =
|
||||||
fetchMock.resetMocks();
|
"https://hub.docker.com/v2/repositories/outlinewiki/outline/tags";
|
||||||
});
|
|
||||||
|
|
||||||
describe("getVersion", () => {
|
describe("getVersion", () => {
|
||||||
it("should return the current version", () => {
|
it("should return the current version", () => {
|
||||||
@@ -17,11 +17,13 @@ describe("getVersionInfo", () => {
|
|||||||
const currentVersion = "0.80.0";
|
const currentVersion = "0.80.0";
|
||||||
|
|
||||||
it("should return version info when Docker Hub is accessible", async () => {
|
it("should return version info when Docker Hub is accessible", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
server.use(
|
||||||
JSON.stringify({
|
http.get(dockerHubUrl, () =>
|
||||||
|
HttpResponse.json({
|
||||||
results: [{ name: "0.81.0" }, { name: "0.80.0" }, { name: "0.79.0" }],
|
results: [{ name: "0.81.0" }, { name: "0.80.0" }, { name: "0.79.0" }],
|
||||||
next: null,
|
next: null,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await getVersionInfo(currentVersion);
|
const result = await getVersionInfo(currentVersion);
|
||||||
@@ -33,7 +35,7 @@ describe("getVersionInfo", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return fallback values when Docker Hub is unreachable", async () => {
|
it("should return fallback values when Docker Hub is unreachable", async () => {
|
||||||
fetchMock.mockRejectOnce(new Error("Network request failed"));
|
server.use(http.get(dockerHubUrl, () => HttpResponse.error()));
|
||||||
|
|
||||||
const result = await getVersionInfo(currentVersion);
|
const result = await getVersionInfo(currentVersion);
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ describe("getVersionInfo", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return fallback values when fetch times out", async () => {
|
it("should return fallback values when fetch times out", async () => {
|
||||||
fetchMock.mockRejectOnce(new Error("Request timeout after 10000ms"));
|
server.use(http.get(dockerHubUrl, () => HttpResponse.error()));
|
||||||
|
|
||||||
const result = await getVersionInfo(currentVersion);
|
const result = await getVersionInfo(currentVersion);
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ describe("getVersionInfo", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return fallback values when DNS lookup fails", async () => {
|
it("should return fallback values when DNS lookup fails", async () => {
|
||||||
fetchMock.mockRejectOnce(new Error("DNS lookup failed"));
|
server.use(http.get(dockerHubUrl, () => HttpResponse.error()));
|
||||||
|
|
||||||
const result = await getVersionInfo(currentVersion);
|
const result = await getVersionInfo(currentVersion);
|
||||||
|
|
||||||
@@ -66,7 +68,12 @@ describe("getVersionInfo", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return fallback values when response is not JSON", async () => {
|
it("should return fallback values when response is not JSON", async () => {
|
||||||
fetchMock.mockResponseOnce("Not JSON");
|
server.use(
|
||||||
|
http.get(
|
||||||
|
dockerHubUrl,
|
||||||
|
() => new HttpResponse("Not JSON", { status: 200 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await getVersionInfo(currentVersion);
|
const result = await getVersionInfo(currentVersion);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Create a lazily-loaded object registry. The loader runs only when the
|
||||||
|
* registry is first accessed.
|
||||||
|
*
|
||||||
|
* @param load A function that returns the registry contents.
|
||||||
|
* @returns A proxy that exposes the lazily-loaded registry.
|
||||||
|
*/
|
||||||
|
export function createLazyRegistry<T>(
|
||||||
|
load: () => Record<string, T>
|
||||||
|
): Record<string, T> {
|
||||||
|
let cache: Record<string, T> | undefined;
|
||||||
|
|
||||||
|
const getRegistry = () => {
|
||||||
|
if (cache) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = load();
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy({} as Record<string, T>, {
|
||||||
|
get(_target, prop: string | symbol) {
|
||||||
|
if (typeof prop === "symbol") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRegistry()[prop];
|
||||||
|
},
|
||||||
|
has(_target, prop: string | symbol) {
|
||||||
|
return typeof prop === "string" && prop in getRegistry();
|
||||||
|
},
|
||||||
|
ownKeys() {
|
||||||
|
return Reflect.ownKeys(getRegistry());
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(_target, prop: string | symbol) {
|
||||||
|
return Reflect.getOwnPropertyDescriptor(getRegistry(), prop);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import { http, HttpResponse } from "msw";
|
||||||
|
import { server } from "@server/test/msw";
|
||||||
import OAuthClient from "./oauth";
|
import OAuthClient from "./oauth";
|
||||||
|
|
||||||
class MinimalOAuthClient extends OAuthClient {
|
class MinimalOAuthClient extends OAuthClient {
|
||||||
@@ -9,16 +10,15 @@ class MinimalOAuthClient extends OAuthClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("userInfo", () => {
|
describe("userInfo", () => {
|
||||||
it("should work with empty-body 401 Unauthorized responses", async () => {
|
it("should work with empty-body 401 Unauthorized responses", async () => {
|
||||||
fetchMock.mockResponseOnce("", {
|
server.use(
|
||||||
status: 401,
|
http.get(
|
||||||
statusText: "unauthorized",
|
"http://example.com/userinfo",
|
||||||
});
|
() =>
|
||||||
|
new HttpResponse(null, { status: 401, statusText: "unauthorized" })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const client = new MinimalOAuthClient("clientid", "clientsecret");
|
const client = new MinimalOAuthClient("clientid", "clientsecret");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { expect } from "@jest/globals";
|
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import parseAttachmentIds from "./parseAttachmentIds";
|
import parseAttachmentIds from "./parseAttachmentIds";
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import dns from "node:dns";
|
import dns from "node:dns";
|
||||||
|
import type { MockInstance } from "vitest";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import { validateUrlNotPrivate } from "./url";
|
import { validateUrlNotPrivate } from "./url";
|
||||||
|
|
||||||
describe("validateUrlNotPrivate", () => {
|
describe("validateUrlNotPrivate", () => {
|
||||||
let lookupSpy: jest.SpyInstance;
|
let lookupSpy: MockInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
lookupSpy = jest
|
lookupSpy = vi
|
||||||
.spyOn(dns.promises, "lookup")
|
.spyOn(dns.promises, "lookup")
|
||||||
.mockResolvedValue({ address: "93.184.216.34", family: 4 });
|
.mockResolvedValue({ address: "93.184.216.34", family: 4 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from "@jest/globals";
|
|
||||||
import { isDatabaseUrl, isMailboxAddress } from "./validators";
|
import { isDatabaseUrl, isMailboxAddress } from "./validators";
|
||||||
|
|
||||||
describe("isDatabaseUrl", () => {
|
describe("isDatabaseUrl", () => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
jest.mock("i18next-http-backend");
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("i18next-http-backend");
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { TextHelper } from "./TextHelper";
|
|||||||
|
|
||||||
describe("TextHelper", () => {
|
describe("TextHelper", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
jest.setSystemTime(Date.parse("2021-01-01T00:00:00.000Z"));
|
vi.setSystemTime(Date.parse("2021-01-01T00:00:00.000Z"));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("replaceTemplateVariables", () => {
|
describe("replaceTemplateVariables", () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { bytesToHumanReadable, getFileNameFromUrl } from "./files";
|
|||||||
// Mock the browser detection with a mutable value
|
// Mock the browser detection with a mutable value
|
||||||
let mockIsMacValue = false;
|
let mockIsMacValue = false;
|
||||||
|
|
||||||
jest.mock("./browser", () => ({
|
vi.mock("./browser", () => ({
|
||||||
get isMac() {
|
get isMac() {
|
||||||
return mockIsMacValue;
|
return mockIsMacValue;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import babel from "vite-plugin-babel";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
const aliases = {
|
||||||
|
"@server": path.resolve(__dirname, "./server"),
|
||||||
|
"@shared": path.resolve(__dirname, "./shared"),
|
||||||
|
"~": path.resolve(__dirname, "./app"),
|
||||||
|
plugins: path.resolve(__dirname, "./plugins"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileMock = path.resolve(__dirname, "./__mocks__/fileMock.js");
|
||||||
|
|
||||||
|
const babelPlugin = () =>
|
||||||
|
babel({
|
||||||
|
filter: /\.(t|j)sx?$/,
|
||||||
|
babelConfig: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
sourceMaps: "inline",
|
||||||
|
presets: [
|
||||||
|
["@babel/preset-react", { runtime: "automatic" }],
|
||||||
|
["@babel/preset-typescript", { allowDeclareFields: true }],
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
"babel-plugin-transform-typescript-metadata",
|
||||||
|
["@babel/plugin-proposal-decorators", { legacy: true }],
|
||||||
|
"@babel/plugin-transform-class-properties",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedConfig = {
|
||||||
|
resolve: { alias: aliases },
|
||||||
|
plugins: [babelPlugin()],
|
||||||
|
esbuild: false as const,
|
||||||
|
oxc: false as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const aliasesAsArray = Object.entries(aliases).map(([find, replacement]) => ({
|
||||||
|
find,
|
||||||
|
replacement,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fileMockAlias = { find: /\.(gif|ttf|eot|svg)$/, replacement: fileMock };
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
pool: "threads",
|
||||||
|
// Match Jest's default behavior — unhandled promise rejections are
|
||||||
|
// logged but don't fail tests on their own.
|
||||||
|
dangerouslyIgnoreUnhandledErrors: true,
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
...sharedConfig,
|
||||||
|
test: {
|
||||||
|
name: "server",
|
||||||
|
globals: true,
|
||||||
|
environment: "node",
|
||||||
|
include: ["server/**/*.test.{ts,tsx}", "plugins/**/*.test.{ts,tsx}"],
|
||||||
|
setupFiles: [
|
||||||
|
"./__mocks__/console.js",
|
||||||
|
"./server/test/setupMocks.ts",
|
||||||
|
"./server/test/setup.ts",
|
||||||
|
],
|
||||||
|
globalSetup: ["./server/test/globalTeardown.ts"],
|
||||||
|
fileParallelism: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...sharedConfig,
|
||||||
|
resolve: { alias: [fileMockAlias, ...aliasesAsArray] },
|
||||||
|
test: {
|
||||||
|
name: "app",
|
||||||
|
globals: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
environmentOptions: {
|
||||||
|
jsdom: { url: "http://localhost" },
|
||||||
|
},
|
||||||
|
include: ["app/**/*.test.{ts,tsx}"],
|
||||||
|
setupFiles: ["./__mocks__/window.js", "./app/test/setup.ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...sharedConfig,
|
||||||
|
test: {
|
||||||
|
name: "shared-node",
|
||||||
|
globals: true,
|
||||||
|
environment: "node",
|
||||||
|
include: ["shared/**/*.test.{ts,tsx}"],
|
||||||
|
setupFiles: ["./__mocks__/console.js", "./shared/test/setup.ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...sharedConfig,
|
||||||
|
resolve: { alias: [fileMockAlias, ...aliasesAsArray] },
|
||||||
|
test: {
|
||||||
|
name: "shared-jsdom",
|
||||||
|
globals: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
environmentOptions: {
|
||||||
|
jsdom: { url: "http://localhost" },
|
||||||
|
},
|
||||||
|
include: ["shared/**/*.test.{ts,tsx}"],
|
||||||
|
setupFiles: ["./__mocks__/window.js"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vitest/globals" />
|
||||||
Reference in New Issue
Block a user