Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
347
packages/core/tests/utils/test-db.ts
Normal file
347
packages/core/tests/utils/test-db.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { Kysely, PostgresDialect, SqliteDialect } from "kysely";
|
||||
import { Pool } from "pg";
|
||||
import { describe } from "vitest";
|
||||
|
||||
import { runMigrations } from "../../src/database/migrations/runner.js";
|
||||
import type { Database as DatabaseSchema } from "../../src/database/types.js";
|
||||
import { SchemaRegistry } from "../../src/schema/registry.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Environment
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* PostgreSQL connection string for tests.
|
||||
* When set, Postgres tests run; when absent, they're skipped.
|
||||
*/
|
||||
export const PG_CONNECTION_STRING = process.env.EMDASH_TEST_PG ?? "";
|
||||
|
||||
/**
|
||||
* Whether a Postgres test database is available.
|
||||
*/
|
||||
export const hasPgTestDatabase = PG_CONNECTION_STRING.length > 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SQLite helpers (unchanged)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create an in-memory SQLite database for testing
|
||||
*/
|
||||
export function createTestDatabase(): Kysely<DatabaseSchema> {
|
||||
const sqlite = new Database(":memory:");
|
||||
|
||||
return new Kysely<DatabaseSchema>({
|
||||
dialect: new SqliteDialect({
|
||||
database: sqlite,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a test database with migrations run
|
||||
*/
|
||||
export async function setupTestDatabase(): Promise<Kysely<DatabaseSchema>> {
|
||||
const db = createTestDatabase();
|
||||
await runMigrations(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a test database with standard test collections (post, page)
|
||||
* This creates the ec_post and ec_page tables with title and content fields
|
||||
*/
|
||||
export async function setupTestDatabaseWithCollections(): Promise<Kysely<DatabaseSchema>> {
|
||||
const db = await setupTestDatabase();
|
||||
const registry = new SchemaRegistry(db);
|
||||
|
||||
// Create post collection
|
||||
await registry.createCollection({
|
||||
slug: "post",
|
||||
label: "Posts",
|
||||
labelSingular: "Post",
|
||||
});
|
||||
await registry.createField("post", {
|
||||
slug: "title",
|
||||
label: "Title",
|
||||
type: "string",
|
||||
});
|
||||
await registry.createField("post", {
|
||||
slug: "content",
|
||||
label: "Content",
|
||||
type: "portableText",
|
||||
});
|
||||
|
||||
// Create page collection
|
||||
await registry.createCollection({
|
||||
slug: "page",
|
||||
label: "Pages",
|
||||
labelSingular: "Page",
|
||||
});
|
||||
await registry.createField("page", {
|
||||
slug: "title",
|
||||
label: "Title",
|
||||
type: "string",
|
||||
});
|
||||
await registry.createField("page", {
|
||||
slug: "content",
|
||||
label: "Content",
|
||||
type: "portableText",
|
||||
});
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and destroy a test database
|
||||
*/
|
||||
export async function teardownTestDatabase(db: Kysely<DatabaseSchema>): Promise<void> {
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PostgreSQL helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Shared pool for Postgres tests. One pool per test process, many schemas.
|
||||
* Created lazily on first call to createTestPostgresDatabase().
|
||||
*/
|
||||
let sharedPool: Pool | null = null;
|
||||
|
||||
function getSharedPool(): Pool {
|
||||
if (!sharedPool) {
|
||||
sharedPool = new Pool({
|
||||
connectionString: PG_CONNECTION_STRING,
|
||||
max: 10,
|
||||
});
|
||||
}
|
||||
return sharedPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique schema name for test isolation.
|
||||
* Format: test_<timestamp>_<random> — short, valid SQL identifier.
|
||||
*/
|
||||
function uniqueSchemaName(): string {
|
||||
const ts = Date.now().toString(36);
|
||||
const rand = Math.random().toString(36).slice(2, 8);
|
||||
return `test_${ts}_${rand}`;
|
||||
}
|
||||
|
||||
export interface PgTestContext {
|
||||
db: Kysely<DatabaseSchema>;
|
||||
schemaName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an isolated Postgres database for a single test.
|
||||
*
|
||||
* Each call creates a unique schema and returns a Kysely instance
|
||||
* whose search_path is set to that schema. Tables are fully isolated.
|
||||
*
|
||||
* Call `teardownTestPostgresDatabase()` in afterEach to drop the schema.
|
||||
*/
|
||||
export async function createTestPostgresDatabase(): Promise<PgTestContext> {
|
||||
const pool = getSharedPool();
|
||||
const schemaName = uniqueSchemaName();
|
||||
|
||||
// Create the isolated schema using a raw connection
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query(`CREATE SCHEMA ${schemaName}`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
// Create a Kysely instance that targets this schema.
|
||||
// Test schema comes first so CREATE TABLE goes there.
|
||||
// public is included for Postgres system functions and extensions.
|
||||
const testPool = new Pool({
|
||||
connectionString: PG_CONNECTION_STRING,
|
||||
max: 5,
|
||||
options: `-c search_path=${schemaName},public`,
|
||||
});
|
||||
|
||||
const db = new Kysely<DatabaseSchema>({
|
||||
dialect: new PostgresDialect({ pool: testPool }),
|
||||
});
|
||||
|
||||
return { db, schemaName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a Postgres test database with migrations run.
|
||||
*/
|
||||
export async function setupTestPostgresDatabase(): Promise<PgTestContext> {
|
||||
const ctx = await createTestPostgresDatabase();
|
||||
await runMigrations(ctx.db);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a Postgres test database with standard test collections (post, page).
|
||||
*/
|
||||
export async function setupTestPostgresDatabaseWithCollections(): Promise<PgTestContext> {
|
||||
const ctx = await setupTestPostgresDatabase();
|
||||
const registry = new SchemaRegistry(ctx.db);
|
||||
|
||||
await registry.createCollection({
|
||||
slug: "post",
|
||||
label: "Posts",
|
||||
labelSingular: "Post",
|
||||
});
|
||||
await registry.createField("post", {
|
||||
slug: "title",
|
||||
label: "Title",
|
||||
type: "string",
|
||||
});
|
||||
await registry.createField("post", {
|
||||
slug: "content",
|
||||
label: "Content",
|
||||
type: "portableText",
|
||||
});
|
||||
|
||||
await registry.createCollection({
|
||||
slug: "page",
|
||||
label: "Pages",
|
||||
labelSingular: "Page",
|
||||
});
|
||||
await registry.createField("page", {
|
||||
slug: "title",
|
||||
label: "Title",
|
||||
type: "string",
|
||||
});
|
||||
await registry.createField("page", {
|
||||
slug: "content",
|
||||
label: "Content",
|
||||
type: "portableText",
|
||||
});
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down a Postgres test database — drops the schema and closes the pool.
|
||||
*/
|
||||
export async function teardownTestPostgresDatabase(ctx: PgTestContext): Promise<void> {
|
||||
// Destroy the test pool first
|
||||
await ctx.db.destroy();
|
||||
|
||||
// Drop the schema using the shared pool
|
||||
const pool = getSharedPool();
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query(`DROP SCHEMA IF EXISTS ${ctx.schemaName} CASCADE`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the shared Postgres pool. Call once at the end of the test run.
|
||||
*/
|
||||
export async function destroySharedPool(): Promise<void> {
|
||||
if (sharedPool) {
|
||||
await sharedPool.end();
|
||||
sharedPool = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dialect-parametric test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type DialectName = "sqlite" | "postgres";
|
||||
|
||||
export interface DialectTestContext {
|
||||
db: Kysely<DatabaseSchema>;
|
||||
dialect: DialectName;
|
||||
/** Only present for Postgres — needed for teardown */
|
||||
pgCtx?: PgTestContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bare test database for a given dialect (no migrations).
|
||||
*/
|
||||
export async function createForDialect(dialect: DialectName): Promise<DialectTestContext> {
|
||||
if (dialect === "postgres") {
|
||||
const pgCtx = await createTestPostgresDatabase();
|
||||
return { db: pgCtx.db, dialect, pgCtx };
|
||||
}
|
||||
const db = createTestDatabase();
|
||||
return { db, dialect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test database for a given dialect (with migrations).
|
||||
*/
|
||||
export async function setupForDialect(dialect: DialectName): Promise<DialectTestContext> {
|
||||
if (dialect === "postgres") {
|
||||
const pgCtx = await setupTestDatabase_pg();
|
||||
return { db: pgCtx.db, dialect, pgCtx };
|
||||
}
|
||||
const db = await setupTestDatabase();
|
||||
return { db, dialect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test database with collections for a given dialect.
|
||||
*/
|
||||
export async function setupForDialectWithCollections(
|
||||
dialect: DialectName,
|
||||
): Promise<DialectTestContext> {
|
||||
if (dialect === "postgres") {
|
||||
const pgCtx = await setupTestPostgresDatabaseWithCollections();
|
||||
return { db: pgCtx.db, dialect, pgCtx };
|
||||
}
|
||||
const db = await setupTestDatabaseWithCollections();
|
||||
return { db, dialect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down a test database for any dialect.
|
||||
*/
|
||||
export async function teardownForDialect(ctx: DialectTestContext): Promise<void> {
|
||||
if (ctx.pgCtx) {
|
||||
await teardownTestPostgresDatabase(ctx.pgCtx);
|
||||
} else {
|
||||
await teardownTestDatabase(ctx.db);
|
||||
}
|
||||
}
|
||||
|
||||
// Private alias to avoid name collision
|
||||
const setupTestDatabase_pg = setupTestPostgresDatabase;
|
||||
|
||||
/**
|
||||
* Run a describe block once per available dialect.
|
||||
*
|
||||
* When EMDASH_TEST_PG is not set, only SQLite runs.
|
||||
* When set, the suite runs for both SQLite and Postgres.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* describeEachDialect("Migrations", (dialectName) => {
|
||||
* let ctx: DialectTestContext;
|
||||
* beforeEach(async () => { ctx = await setupForDialect(dialectName); });
|
||||
* afterEach(async () => { await teardownForDialect(ctx); });
|
||||
*
|
||||
* it("creates tables", async () => {
|
||||
* // ctx.db works with either dialect
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function describeEachDialect(name: string, fn: (dialect: DialectName) => void): void {
|
||||
const dialects: DialectName[] = ["sqlite"];
|
||||
if (hasPgTestDatabase) {
|
||||
dialects.push("postgres");
|
||||
}
|
||||
|
||||
for (const dialect of dialects) {
|
||||
describe(`${name} [${dialect}]`, () => {
|
||||
fn(dialect);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user