Builds the deferred integration tests from docs/backlog/feature-test-coverage- deferred.md that needed real infra (a test Postgres + real Redis) which the repo lacked. Runs against an isolated, auto-created docmost_test database and Redis logical DB 15 — never the dev data. Harness (apps/server/test/integration/, run via new `pnpm --filter server test:int` => jest --config test/jest-integration.json; default unit `jest` is untouched and excludes these via the *.int-spec.ts name + rootDir): - db.ts: buildTestDb() mirrors database.module.ts exactly (PostgresJSDialect, CamelCasePlugin, bigint to:20/from:[20,1700] parsing) + minimal seed helpers. - global-setup.ts: DROP/CREATE docmost_test, CREATE EXTENSION vector, migrate to latest via Kysely Migrator (fails loud on any errored migration). - global-teardown.ts: closes the pool. Coverage (5 suites, 16 tests, all green against live PG+Redis): - WorkspaceRepo.updateSetting: jsonb-merge persists htmlEmbed without clobbering sibling ai/sharing namespaces (the kill-switch write half). - AiAgentRoleRepo: soft-delete exclusion, cross-workspace tenant isolation, duplicate (name,workspace) -> 23505, name reusable after softDelete (partial unique index WHERE deleted_at IS NULL), same name across workspaces allowed. - page_template_references: deleting either source or referenced page cascades the link row (onDelete cascade) — real FK, not mocked. - PublicShareWorkspaceLimiter vs REAL Redis: real ioredis EVAL of the sliding- window Lua — max boundary (3 admit / 4th deny), re-admit after the window slides, same-ms distinct members. Catches Lua bugs a FakeRedis cannot. - AiChatRepo.findByCreator: role-badge join (enabled->badge; soft-deleted or disabled role -> null). Review: APPROVE; applied its two hardening suggestions (fail loud on errored migration result even without a top-level error; TEST_REDIS_URL override + ping preflight). tsc clean; unit run excludes int-spec (verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
import * as path from 'node:path';
|
|
import { promises as fs } from 'node:fs';
|
|
import { Kysely, Migrator, FileMigrationProvider } from 'kysely';
|
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
|
import * as postgres from 'postgres';
|
|
import { TEST_DATABASE_URL, buildTestDb } from './db';
|
|
|
|
const MAINTENANCE_URL =
|
|
process.env.TEST_MAINTENANCE_DATABASE_URL ??
|
|
'postgresql://docmost:docmost_dev_pw@localhost:5432/docmost';
|
|
|
|
const TEST_DB_NAME = 'docmost_test';
|
|
|
|
// migrate.ts points FileMigrationProvider at src/database/migrations; mirror it.
|
|
const migrationFolder = path.resolve(
|
|
__dirname,
|
|
'../../src/database/migrations',
|
|
);
|
|
|
|
/**
|
|
* Jest globalSetup: (re)create the isolated test database and migrate it to
|
|
* latest. Mirrors apps/server/src/database/migrate.ts (Kysely Migrator +
|
|
* FileMigrationProvider) so the schema is exactly what the app expects.
|
|
*/
|
|
export default async function globalSetup(): Promise<void> {
|
|
// 1. DROP/CREATE the test DB via the maintenance connection. These statements
|
|
// cannot run inside a transaction; use the raw postgres client's simple
|
|
// query (`.simple()`) so the driver does not wrap them.
|
|
const maintenance = postgres(MAINTENANCE_URL, { max: 1, onnotice: () => {} });
|
|
try {
|
|
await maintenance`DROP DATABASE IF EXISTS docmost_test WITH (FORCE)`.simple();
|
|
await maintenance`CREATE DATABASE docmost_test`.simple();
|
|
} finally {
|
|
await maintenance.end({ timeout: 5 });
|
|
}
|
|
|
|
// 2. Enable pgvector on the fresh DB (migrations create vector columns).
|
|
const ext = postgres(TEST_DATABASE_URL, { max: 1, onnotice: () => {} });
|
|
try {
|
|
await ext`CREATE EXTENSION IF NOT EXISTS vector`.simple();
|
|
} finally {
|
|
await ext.end({ timeout: 5 });
|
|
}
|
|
|
|
// 3. Run all migrations to latest against docmost_test.
|
|
const db: Kysely<any> = new Kysely<any>({
|
|
dialect: new PostgresJSDialect({
|
|
postgres: postgres(TEST_DATABASE_URL, { onnotice: () => {} }),
|
|
}),
|
|
});
|
|
const migrator = new Migrator({
|
|
db,
|
|
provider: new FileMigrationProvider({ fs, path, migrationFolder }),
|
|
});
|
|
|
|
const { error, results } = await migrator.migrateToLatest();
|
|
// Fail loud on ANY errored migration, even if Migrator did not also surface a
|
|
// top-level `error` — never run the suite against a half-migrated schema.
|
|
const failed = (results ?? []).filter((r) => r.status === 'Error');
|
|
await db.destroy();
|
|
|
|
if (error || failed.length > 0) {
|
|
const names = failed.map((r) => r.migrationName).join(', ');
|
|
throw new Error(
|
|
`Test DB migration failed${names ? ` (${names})` : ''}: ${
|
|
(error as Error)?.message ?? error ?? 'errored migration result'
|
|
}`,
|
|
);
|
|
}
|
|
|
|
// 4. Pin the URL for the test workers (db.ts reads it from env).
|
|
process.env.TEST_DATABASE_URL = TEST_DATABASE_URL;
|
|
|
|
// Sanity touch: open + close the shared test Kysely once so a bad connection
|
|
// surfaces here rather than mid-suite.
|
|
const probe = buildTestDb();
|
|
await probe.selectFrom('workspaces').select('id').limit(1).execute();
|
|
await probe.destroy();
|
|
}
|