mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
test: add page hierarchy cycle integration harness
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
"migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/database/types/db.d.ts",
|
"migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/database/types/db.d.ts",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:integration": "jest --config test/jest-integration.json --runInBand",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: docmost-cycle-test
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: docmost_cycle_test
|
||||||
|
POSTGRES_USER: docmost
|
||||||
|
POSTGRES_PASSWORD: docmost
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:55432:5432'
|
||||||
|
tmpfs:
|
||||||
|
- /var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U docmost -d docmost_cycle_test']
|
||||||
|
interval: 2s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 15
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".*\\.integration-spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"happy-dom.+\\.js$": [
|
||||||
|
"babel-jest",
|
||||||
|
{
|
||||||
|
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"/node_modules/(?!(\\.pnpm/)?(nanoid|uuid|image-dimensions|marked|happy-dom)(@|/))"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@docmost/db/(.*)$": "<rootDir>/../src/database/$1",
|
||||||
|
"^@docmost/transactional/(.*)$": "<rootDir>/../src/integrations/transactional/$1",
|
||||||
|
"^@docmost/ee/(.*)$": "<rootDir>/../src/ee/$1",
|
||||||
|
"^src/(.*)$": "<rootDir>/../src/$1",
|
||||||
|
"^@docmost/base-formula/server$": "<rootDir>/../../../packages/base-formula/src/index.server.ts",
|
||||||
|
"^@docmost/base-formula/client$": "<rootDir>/../../../packages/base-formula/src/index.client.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { db } from './support/database';
|
||||||
|
import { seedAcyclicPageChain } from './support/page-hierarchy-fixtures';
|
||||||
|
|
||||||
|
describe('page hierarchy cycle integration harness', () => {
|
||||||
|
it('inserts and reads an acyclic page chain through the test Kysely instance', async () => {
|
||||||
|
const { root, child, grandchild } = await seedAcyclicPageChain();
|
||||||
|
|
||||||
|
const pages = await db
|
||||||
|
.selectFrom('pages')
|
||||||
|
.select(['id', 'parentPageId', 'title'])
|
||||||
|
.where('id', 'in', [root.id, child.id, grandchild.id])
|
||||||
|
.orderBy('title')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
expect(pages).toEqual([
|
||||||
|
{ id: child.id, parentPageId: root.id, title: 'Child' },
|
||||||
|
{ id: grandchild.id, parentPageId: child.id, title: 'Grandchild' },
|
||||||
|
{ id: root.id, parentPageId: null, title: 'Root' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { CamelCasePlugin, Kysely, sql } from 'kysely';
|
||||||
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
|
import * as postgres from 'postgres';
|
||||||
|
import { DbInterface } from '../../src/database/types/db.interface';
|
||||||
|
|
||||||
|
const databaseUrl = process.env.TEST_DATABASE_URL;
|
||||||
|
|
||||||
|
if (!databaseUrl) {
|
||||||
|
throw new Error('TEST_DATABASE_URL must be set for integration tests');
|
||||||
|
}
|
||||||
|
|
||||||
|
const postgresPool = postgres(databaseUrl, { max: 1, onnotice: () => {} });
|
||||||
|
|
||||||
|
export const db = new Kysely<DbInterface>({
|
||||||
|
dialect: new PostgresJSDialect({ postgres: postgresPool }),
|
||||||
|
plugins: [new CamelCasePlugin()],
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function withStatementTimeout<T>(
|
||||||
|
callback: (connection: Kysely<DbInterface>) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
return db.connection().execute(async (connection) => {
|
||||||
|
await sql`SET statement_timeout = '500ms'`.execute(connection);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await callback(connection);
|
||||||
|
} finally {
|
||||||
|
await sql`SET statement_timeout = DEFAULT`.execute(connection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function truncateFixtureTables(): Promise<void> {
|
||||||
|
await sql`TRUNCATE TABLE pages, spaces, workspaces CASCADE`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncateFixtureTables();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await postgresPool.end();
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { sql } from 'kysely';
|
||||||
|
import { db } from './database';
|
||||||
|
|
||||||
|
export type PageFixture = {
|
||||||
|
id: string;
|
||||||
|
parentPageId: string | null;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HierarchyContext = {
|
||||||
|
workspaceId: string;
|
||||||
|
spaceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function seedWorkspaceAndSpace(): Promise<HierarchyContext> {
|
||||||
|
const workspace = await db
|
||||||
|
.insertInto('workspaces')
|
||||||
|
.values({
|
||||||
|
hostname: `cycle-test-${randomUUID()}`,
|
||||||
|
name: 'Page hierarchy integration workspace',
|
||||||
|
})
|
||||||
|
.returning('id')
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const space = await db
|
||||||
|
.insertInto('spaces')
|
||||||
|
.values({
|
||||||
|
name: 'Page hierarchy integration space',
|
||||||
|
slug: `cycle-test-${randomUUID()}`,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
})
|
||||||
|
.returning('id')
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return { spaceId: space.id, workspaceId: workspace.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertPage(
|
||||||
|
context: HierarchyContext,
|
||||||
|
title: string,
|
||||||
|
parentPageId: string | null = null,
|
||||||
|
): Promise<PageFixture> {
|
||||||
|
const page = await db
|
||||||
|
.insertInto('pages')
|
||||||
|
.values({
|
||||||
|
parentPageId,
|
||||||
|
slugId: randomUUID(),
|
||||||
|
spaceId: context.spaceId,
|
||||||
|
title,
|
||||||
|
workspaceId: context.workspaceId,
|
||||||
|
})
|
||||||
|
.returning(['id', 'parentPageId', 'title'])
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return { id: page.id, parentPageId: page.parentPageId, title };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedAcyclicPageChain(): Promise<{
|
||||||
|
root: PageFixture;
|
||||||
|
child: PageFixture;
|
||||||
|
grandchild: PageFixture;
|
||||||
|
}> {
|
||||||
|
const context = await seedWorkspaceAndSpace();
|
||||||
|
const root = await insertPage(context, 'Root');
|
||||||
|
const child = await insertPage(context, 'Child', root.id);
|
||||||
|
const grandchild = await insertPage(context, 'Grandchild', child.id);
|
||||||
|
|
||||||
|
return { root, child, grandchild };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedSelfCycle(): Promise<{ self: PageFixture }> {
|
||||||
|
const context = await seedWorkspaceAndSpace();
|
||||||
|
const self = await insertPage(context, 'Self');
|
||||||
|
|
||||||
|
await sql`UPDATE pages SET parent_page_id = ${self.id} WHERE id = ${self.id}`.execute(
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { self: { ...self, parentPageId: self.id } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedTwoPageCycle(): Promise<{
|
||||||
|
a: PageFixture;
|
||||||
|
b: PageFixture;
|
||||||
|
}> {
|
||||||
|
const context = await seedWorkspaceAndSpace();
|
||||||
|
const a = await insertPage(context, 'A');
|
||||||
|
const b = await insertPage(context, 'B', a.id);
|
||||||
|
|
||||||
|
await sql`UPDATE pages SET parent_page_id = ${b.id} WHERE id = ${a.id}`.execute(
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
a: { ...a, parentPageId: b.id },
|
||||||
|
b,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedBranchingDescendantTree(): Promise<{
|
||||||
|
root: PageFixture;
|
||||||
|
firstChild: PageFixture;
|
||||||
|
secondChild: PageFixture;
|
||||||
|
grandchild: PageFixture;
|
||||||
|
}> {
|
||||||
|
const context = await seedWorkspaceAndSpace();
|
||||||
|
const root = await insertPage(context, 'Root');
|
||||||
|
const firstChild = await insertPage(context, 'First child', root.id);
|
||||||
|
const secondChild = await insertPage(context, 'Second child', root.id);
|
||||||
|
const grandchild = await insertPage(context, 'Grandchild', firstChild.id);
|
||||||
|
|
||||||
|
return { root, firstChild, secondChild, grandchild };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user