Refactor test database setup and add migrations
- Updated test files to use a separate test database configuration. - Introduced a new helper module for managing test database operations. - Added a setup file to configure the environment for tests. - Created new database migration scripts to optimize message indexing. - Added a sample environment file for test database configuration.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import process from "node:process";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { assertSafeTestDatabaseUrl } from "./helpers/testDatabase";
|
||||
|
||||
const originalEnv = process.env;
|
||||
|
||||
@@ -14,6 +15,10 @@ describe("Drizzle ORM Database", () => {
|
||||
DISCORD_TOKEN: "test-token",
|
||||
NODE_ENV: "test",
|
||||
};
|
||||
if (originalEnv.TEST_DATABASE_URL) {
|
||||
process.env.DATABASE_URL = originalEnv.TEST_DATABASE_URL;
|
||||
}
|
||||
assertSafeTestDatabaseUrl();
|
||||
|
||||
// Reset modules to pick up new environment
|
||||
vi.resetModules();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import process from "node:process";
|
||||
import {
|
||||
getDatabase,
|
||||
initializeDatabase,
|
||||
} from "../../src/database/drizzle";
|
||||
|
||||
interface RunnableDatabase {
|
||||
run(sql: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
const SAFE_TEST_DATABASE_NAME = /(^|[_-])(test|testing)([_-]|$)|gmw_test/i;
|
||||
|
||||
function getDatabaseNameFromUrl(databaseUrl: string): string {
|
||||
try {
|
||||
const parsed = new URL(databaseUrl);
|
||||
return parsed.pathname.replace(/^\//, "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getConfiguredDatabaseName(): string {
|
||||
if (process.env.DATABASE_URL) {
|
||||
return getDatabaseNameFromUrl(process.env.DATABASE_URL);
|
||||
}
|
||||
return process.env.POSTGRES_DB ?? "";
|
||||
}
|
||||
|
||||
export function assertSafeTestDatabaseUrl(): void {
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
throw new Error(
|
||||
`Refusing to run destructive database test outside NODE_ENV=test (got ${process.env.NODE_ENV ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.TEST_DATABASE_URL) {
|
||||
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
||||
}
|
||||
|
||||
const databaseName = getConfiguredDatabaseName();
|
||||
if (!SAFE_TEST_DATABASE_NAME.test(databaseName)) {
|
||||
throw new Error(
|
||||
`Refusing to run destructive database test against non-test database "${databaseName || "unknown"}". Set TEST_DATABASE_URL or DATABASE_URL to a database whose name contains "test" (for example hub_test).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeTestDatabase() {
|
||||
assertSafeTestDatabaseUrl();
|
||||
return initializeDatabase();
|
||||
}
|
||||
|
||||
export function getTestDatabase(): RunnableDatabase {
|
||||
assertSafeTestDatabaseUrl();
|
||||
return getDatabase() as unknown as RunnableDatabase;
|
||||
}
|
||||
|
||||
export async function clearTestTables(...tableNames: string[]): Promise<void> {
|
||||
assertSafeTestDatabaseUrl();
|
||||
const db = getTestDatabase();
|
||||
for (const tableName of tableNames) {
|
||||
await db.run(`DELETE FROM "${tableName}"`);
|
||||
}
|
||||
}
|
||||
@@ -686,9 +686,7 @@ describe("runModerationAnalysis", () => {
|
||||
expect(secondRequestBody.messages[0].content).toContain(
|
||||
"RESPON SEBELUMNYA GAGAL VALIDASI",
|
||||
);
|
||||
expect(secondRequestBody.messages[0].content).toContain(
|
||||
"Invalid option",
|
||||
);
|
||||
expect(secondRequestBody.messages[0].content).toContain("Invalid option");
|
||||
expect(secondRequestBody.messages[0].content).toContain(
|
||||
"Coba lagi dengan output JSON yang benar",
|
||||
);
|
||||
@@ -753,8 +751,8 @@ describe("runModerationAnalysis", () => {
|
||||
arrayBuffer: async () => {
|
||||
// Minimal valid PNG bytes (8-byte signature)
|
||||
const png = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
|
||||
0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
]);
|
||||
return png.buffer.slice(
|
||||
png.byteOffset,
|
||||
@@ -932,7 +930,8 @@ describe("runModerationAnalysis", () => {
|
||||
status: "warn",
|
||||
flags: ["harassment"],
|
||||
score: 0.65,
|
||||
analysis: "Teks mengandung unsur harassment dan memerlukan tindakan lebih lanjut.",
|
||||
analysis:
|
||||
"Teks mengandung unsur harassment dan memerlukan tindakan lebih lanjut.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -941,7 +940,22 @@ describe("runModerationAnalysis", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const imageBytes = Buffer.from("realistic-image-bytes");
|
||||
const imageBytes = Buffer.from([
|
||||
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
|
||||
]);
|
||||
const toBody = (buffer: Buffer) => {
|
||||
let done = false;
|
||||
return {
|
||||
getReader: () => ({
|
||||
read: async () => {
|
||||
if (done) return { done: true, value: undefined };
|
||||
done = true;
|
||||
return { done: false, value: new Uint8Array(buffer) };
|
||||
},
|
||||
cancel: vi.fn(),
|
||||
}),
|
||||
};
|
||||
};
|
||||
global.fetch = vi.fn().mockImplementation((url: string) => {
|
||||
if (
|
||||
url === "https://httpbin.org/image/png" ||
|
||||
@@ -949,11 +963,7 @@ describe("runModerationAnalysis", () => {
|
||||
) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
arrayBuffer: async () =>
|
||||
imageBytes.buffer.slice(
|
||||
imageBytes.byteOffset,
|
||||
imageBytes.byteOffset + imageBytes.byteLength,
|
||||
),
|
||||
body: toBody(imageBytes),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { closeDatabase } from "../../src/database/drizzle";
|
||||
import {
|
||||
closeDatabase,
|
||||
getDatabase,
|
||||
initializeDatabase,
|
||||
} from "../../src/database/drizzle";
|
||||
clearTestTables,
|
||||
getTestDatabase,
|
||||
initializeTestDatabase,
|
||||
} from "../helpers/testDatabase";
|
||||
import { captureMessage } from "../../src/moderation/messageCapture";
|
||||
import type { ModerationBroadcaster } from "../../src/moderation/types";
|
||||
|
||||
@@ -22,14 +23,6 @@ type ModerationTestGlobal = typeof globalThis & {
|
||||
moderationBroadcaster?: Partial<ModerationBroadcaster>;
|
||||
};
|
||||
|
||||
interface TestDatabase {
|
||||
run(sql: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
function getTestDatabase(): TestDatabase {
|
||||
return getDatabase() as unknown as TestDatabase;
|
||||
}
|
||||
|
||||
vi.mock("../../src/moderation/aiAnalyzer", () => ({
|
||||
queueMessageAnalysis: (id: string) => queueMessageAnalysis(id),
|
||||
}));
|
||||
@@ -118,15 +111,13 @@ async function createTables() {
|
||||
|
||||
describe("captureMessage", () => {
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase();
|
||||
await initializeTestDatabase();
|
||||
await createTables();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
queueMessageAnalysis.mockClear();
|
||||
const db = getTestDatabase();
|
||||
await db.run(`DELETE FROM "attachments"`);
|
||||
await db.run(`DELETE FROM "messages"`);
|
||||
await clearTestTables("attachments", "messages");
|
||||
delete (globalThis as ModerationTestGlobal).moderationBroadcaster;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { closeDatabase } from "../../src/database/drizzle";
|
||||
import {
|
||||
closeDatabase,
|
||||
getDatabase,
|
||||
initializeDatabase,
|
||||
} from "../../src/database/drizzle";
|
||||
clearTestTables,
|
||||
getTestDatabase,
|
||||
initializeTestDatabase,
|
||||
} from "../helpers/testDatabase";
|
||||
import { createChildLogger } from "../../src/logger";
|
||||
import {
|
||||
decodeCursor,
|
||||
@@ -18,14 +19,6 @@ import {
|
||||
} from "../../src/moderation/messageStore";
|
||||
import type { MessageRecord } from "../../src/moderation/types";
|
||||
|
||||
interface TestDatabase {
|
||||
run(sql: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
function getTestDatabase(): TestDatabase {
|
||||
return getDatabase() as unknown as TestDatabase;
|
||||
}
|
||||
|
||||
const logger = createChildLogger("messageStoreQueries.test");
|
||||
|
||||
describe("message cursor helpers", () => {
|
||||
@@ -44,7 +37,7 @@ describe("message cursor helpers", () => {
|
||||
|
||||
describe("message query integration tests", () => {
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase();
|
||||
await initializeTestDatabase();
|
||||
// Create tables directly for isolated query integration tests
|
||||
const db = getTestDatabase();
|
||||
try {
|
||||
@@ -110,9 +103,7 @@ describe("message query integration tests", () => {
|
||||
beforeEach(async () => {
|
||||
// Clear tables before each test
|
||||
try {
|
||||
const db = getTestDatabase();
|
||||
await db.run(`DELETE FROM "attachments"`);
|
||||
await db.run(`DELETE FROM "messages"`);
|
||||
await clearTestTables("attachments", "messages");
|
||||
} catch (error) {
|
||||
logger.debug({ error }, "Could not clear tables");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import process from "node:process";
|
||||
|
||||
process.env.NODE_ENV = "test";
|
||||
|
||||
if (process.env.TEST_DATABASE_URL) {
|
||||
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
||||
}
|
||||
Reference in New Issue
Block a user