mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
test: guard integration database cleanup
This commit is contained in:
@@ -36,13 +36,18 @@ describe('page hierarchy cycle contract', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(PageHierarchyCycleError);
|
||||
expect((error as PageHierarchyCycleError).rootPageId).toBe('root-page');
|
||||
expect((error as PageHierarchyCycleError).code).toBe('PAGE_HIERARCHY_CYCLE');
|
||||
expect((error as PageHierarchyCycleError).code).toBe(
|
||||
'PAGE_HIERARCHY_CYCLE',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('removes traversal metadata without changing the public row fields', () => {
|
||||
const row = { id: 'page-1', title: 'Root', isCycle: false };
|
||||
|
||||
expect(stripPageTraversalMetadata(row)).toEqual({ id: 'page-1', title: 'Root' });
|
||||
expect(stripPageTraversalMetadata(row)).toEqual({
|
||||
id: 'page-1',
|
||||
title: 'Root',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,11 @@ import { SpaceMemberRepo } from '../src/database/repos/space/space-member.repo';
|
||||
import { User } from '../src/database/types/entity.types';
|
||||
import { KyselyDB } from '../src/database/types/kysely.types';
|
||||
import { HealthController } from '../src/integrations/health/health.controller';
|
||||
import { db, withStatementTimeout } from './support/database';
|
||||
import {
|
||||
assertDisposableTestDatabaseUrl,
|
||||
db,
|
||||
withStatementTimeout,
|
||||
} from './support/database';
|
||||
import {
|
||||
seedAcyclicPageChain,
|
||||
seedBranchingDescendantTree,
|
||||
@@ -322,6 +326,25 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('cycle-safe page hierarchy reads', () => {
|
||||
describe('integration database safety', () => {
|
||||
it.each([
|
||||
'postgresql://docmost:docmost@127.0.0.1:55432/docmost',
|
||||
'postgresql://docmost:docmost@database.example/docmost_cycle_test',
|
||||
])('rejects a non-disposable database URL: %s', (unsafeUrl) => {
|
||||
expect(() => assertDisposableTestDatabaseUrl(unsafeUrl)).toThrow(
|
||||
'Integration tests require the loopback database docmost_cycle_test',
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the dedicated loopback integration database URL', () => {
|
||||
expect(() =>
|
||||
assertDisposableTestDatabaseUrl(
|
||||
'postgresql://docmost:docmost@127.0.0.1:55432/docmost_cycle_test',
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('descendant traversal', () => {
|
||||
it('returns every page in an acyclic branching tree exactly once without internal metadata', async () => {
|
||||
const { root, firstChild, secondChild, grandchild } =
|
||||
|
||||
@@ -3,12 +3,39 @@ import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import * as postgres from 'postgres';
|
||||
import { DbInterface } from '../../src/database/types/db.interface';
|
||||
|
||||
const disposableDatabaseName = 'docmost_cycle_test';
|
||||
const databaseUrl = process.env.TEST_DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
throw new Error('TEST_DATABASE_URL must be set for integration tests');
|
||||
}
|
||||
|
||||
export function assertDisposableTestDatabaseUrl(value: string): void {
|
||||
let parsedUrl: URL;
|
||||
|
||||
try {
|
||||
parsedUrl = new URL(value);
|
||||
} catch {
|
||||
throw new Error('TEST_DATABASE_URL must be a valid PostgreSQL URL');
|
||||
}
|
||||
|
||||
const isPostgresUrl = ['postgres:', 'postgresql:'].includes(
|
||||
parsedUrl.protocol,
|
||||
);
|
||||
const isLoopback = ['127.0.0.1', 'localhost', '[::1]'].includes(
|
||||
parsedUrl.hostname,
|
||||
);
|
||||
const databaseName = decodeURIComponent(parsedUrl.pathname.slice(1));
|
||||
|
||||
if (!isPostgresUrl || !isLoopback || databaseName !== disposableDatabaseName) {
|
||||
throw new Error(
|
||||
`Integration tests require the loopback database ${disposableDatabaseName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assertDisposableTestDatabaseUrl(databaseUrl);
|
||||
|
||||
const postgresPool = postgres(databaseUrl, { max: 1, onnotice: () => {} });
|
||||
|
||||
export const db = new Kysely<DbInterface>({
|
||||
@@ -16,6 +43,22 @@ export const db = new Kysely<DbInterface>({
|
||||
plugins: [new CamelCasePlugin()],
|
||||
});
|
||||
|
||||
let databaseSafetyVerified = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
const result = await sql<{ databaseName: string }>`
|
||||
SELECT current_database() AS "databaseName"
|
||||
`.execute(db);
|
||||
|
||||
if (result.rows[0]?.databaseName !== disposableDatabaseName) {
|
||||
throw new Error(
|
||||
`Connected database must be the disposable ${disposableDatabaseName} database`,
|
||||
);
|
||||
}
|
||||
|
||||
databaseSafetyVerified = true;
|
||||
});
|
||||
|
||||
export async function withStatementTimeout<T>(
|
||||
callback: (connection: Kysely<DbInterface>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
@@ -31,6 +74,10 @@ export async function withStatementTimeout<T>(
|
||||
}
|
||||
|
||||
export async function truncateFixtureTables(): Promise<void> {
|
||||
if (!databaseSafetyVerified) {
|
||||
throw new Error('Refusing to truncate an unverified integration database');
|
||||
}
|
||||
|
||||
await sql`TRUNCATE TABLE pages, spaces, workspaces CASCADE`.execute(db);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user