Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 176 additions & 9 deletions tests/unit/db/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
import { afterEach, describe, expect, it } from 'vitest';
import { _setTestDb, getDb } from '../../../src/db/client.js';

/**
* Tests for the _setTestDb override mechanism in getDb().
* These tests only exercise the override path (where _testDbOverride !== null),
* so no real database connection is needed.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

// ── Hoisted mocks ─────────────────────────────────────────────────────────────
// vi.mock factories are hoisted to the top of the file, so any variables they
// reference must also be hoisted via vi.hoisted().

const { mockPoolEnd, mockPoolConstructor } = vi.hoisted(() => {
const mockPoolEnd = vi.fn().mockResolvedValue(undefined);
const mockPoolConstructor = vi.fn().mockImplementation(() => ({ end: mockPoolEnd }));
return { mockPoolEnd, mockPoolConstructor };
});

vi.mock('pg', () => ({
default: {
Pool: mockPoolConstructor,
},
}));

vi.mock('drizzle-orm/node-postgres', () => ({
drizzle: vi.fn().mockReturnValue({ __isMockDrizzle: true }),
}));

// ── Imports (after mocks) ─────────────────────────────────────────────────────

import { _setTestDb, closeDb, getDb } from '../../../src/db/client.js';

// ── Helpers ───────────────────────────────────────────────────────────────────

/** Reset module-level pool/db singletons between tests. */
async function resetDbState() {
// closeDb() resets pool + db to null; if pool is null it's a no-op so safe.
await closeDb();
// Also clear the test override.
_setTestDb(null);
}

// ── Tests: _setTestDb (pre-existing coverage, kept for regression) ────────────

describe('_setTestDb', () => {
afterEach(() => {
// Always clear to avoid polluting subsequent tests (isolate: false)
_setTestDb(null);
});

Expand Down Expand Up @@ -40,3 +69,141 @@ describe('_setTestDb', () => {
expect(getDb()).toBe(newDb);
});
});

// ── Tests: getDatabaseUrl (tested via getDb internals) ───────────────────────

describe('getDatabaseUrl', () => {
beforeEach(async () => {
await resetDbState();
});

afterEach(async () => {
await resetDbState();
});

it('uses DATABASE_URL when set', () => {
vi.stubEnv('DATABASE_URL', 'postgresql://user:pass@myhost:5432/mydb');
vi.stubEnv('CASCADE_POSTGRES_HOST', '');

getDb();

expect(mockPoolConstructor).toHaveBeenCalledWith(
expect.objectContaining({
connectionString: 'postgresql://user:pass@myhost:5432/mydb',
}),
);
});

it('falls back to CASCADE_POSTGRES_* vars with correct defaults', () => {
vi.stubEnv('DATABASE_URL', '');
vi.stubEnv('CASCADE_POSTGRES_HOST', 'pg.example.com');
vi.stubEnv('CASCADE_POSTGRES_PORT', '');
vi.stubEnv('CASCADE_POSTGRES_USER', '');
vi.stubEnv('CASCADE_POSTGRES_PASSWORD', '');
vi.stubEnv('CASCADE_POSTGRES_DB', '');

getDb();

expect(mockPoolConstructor).toHaveBeenCalledWith(
expect.objectContaining({
connectionString: 'postgresql://postgres:@pg.example.com:6543/cascade',
}),
);
});

it('respects custom CASCADE_POSTGRES_* values', () => {
vi.stubEnv('DATABASE_URL', '');
vi.stubEnv('CASCADE_POSTGRES_HOST', 'custom-host');
vi.stubEnv('CASCADE_POSTGRES_PORT', '5432');
vi.stubEnv('CASCADE_POSTGRES_USER', 'myuser');
vi.stubEnv('CASCADE_POSTGRES_PASSWORD', 'secret');
vi.stubEnv('CASCADE_POSTGRES_DB', 'mydb');

getDb();

expect(mockPoolConstructor).toHaveBeenCalledWith(
expect.objectContaining({
connectionString: 'postgresql://myuser:secret@custom-host:5432/mydb',
}),
);
});

it('throws when neither DATABASE_URL nor CASCADE_POSTGRES_HOST is set', () => {
vi.stubEnv('DATABASE_URL', '');
vi.stubEnv('CASCADE_POSTGRES_HOST', '');

expect(() => getDb()).toThrow('DATABASE_URL or CASCADE_POSTGRES_HOST must be set');
});
});

// ── Tests: getDb ─────────────────────────────────────────────────────────────

describe('getDb', () => {
beforeEach(async () => {
await resetDbState();
vi.stubEnv('DATABASE_URL', 'postgresql://user:pass@localhost:5432/testdb');
});

afterEach(async () => {
await resetDbState();
});

it('creates pool with SSL disabled when DATABASE_SSL=false', () => {
vi.stubEnv('DATABASE_SSL', 'false');

getDb();

expect(mockPoolConstructor).toHaveBeenCalledWith(expect.objectContaining({ ssl: false }));
});

it('creates pool with rejectUnauthorized:false by default (DATABASE_SSL not set)', () => {
vi.stubEnv('DATABASE_SSL', '');

getDb();

expect(mockPoolConstructor).toHaveBeenCalledWith(
expect.objectContaining({ ssl: { rejectUnauthorized: false } }),
);
});

it('returns singleton — second call returns same instance', () => {
const first = getDb();
const second = getDb();

expect(first).toBe(second);
// Pool constructor should only be called once
expect(mockPoolConstructor).toHaveBeenCalledTimes(1);
});
});

// ── Tests: closeDb ────────────────────────────────────────────────────────────

describe('closeDb', () => {
beforeEach(async () => {
await resetDbState();
vi.stubEnv('DATABASE_URL', 'postgresql://user:pass@localhost:5432/testdb');
});

afterEach(async () => {
await resetDbState();
});

it('calls pool.end() and resets state', async () => {
getDb(); // creates pool
expect(mockPoolConstructor).toHaveBeenCalledTimes(1);

await closeDb();
expect(mockPoolEnd).toHaveBeenCalledTimes(1);

// After close, calling getDb() should create a new pool
getDb();
expect(mockPoolConstructor).toHaveBeenCalledTimes(2);
});

it('is a no-op when pool is already null', async () => {
// No getDb() call — pool is null
await closeDb();

expect(mockPoolEnd).not.toHaveBeenCalled();
});
});
85 changes: 85 additions & 0 deletions tests/unit/utils/logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { LOG_LEVELS, getLogLevel, logger, setLogLevel } from '../../../src/utils/logging.js';

/**
* Tests for setLogLevel and getLogLevel in utils/logging.ts.
* The logger is a real llmist Logger instance; we mutate its settings.minLevel
* and restore it between tests.
*/
describe('setLogLevel / getLogLevel', () => {
let originalLevel: number;

beforeEach(() => {
// Capture the current level so we can restore it after each test
originalLevel = logger.settings.minLevel;
});

afterEach(() => {
logger.settings.minLevel = originalLevel;
});

// ── setLogLevel ─────────────────────────────────────────────────────────────

describe('setLogLevel', () => {
it('sets the correct numeric level for each valid level string', () => {
for (const [levelName, levelValue] of Object.entries(LOG_LEVELS)) {
setLogLevel(levelName);
expect(logger.settings.minLevel).toBe(levelValue);
}
});

it('accepts level strings in any case (case-insensitive)', () => {
setLogLevel('DEBUG');
expect(logger.settings.minLevel).toBe(LOG_LEVELS.debug);

setLogLevel('WARN');
expect(logger.settings.minLevel).toBe(LOG_LEVELS.warn);

setLogLevel('Error');
expect(logger.settings.minLevel).toBe(LOG_LEVELS.error);
});

it('ignores invalid level string — leaves minLevel unchanged', () => {
setLogLevel('info'); // set to a known level first
const beforeLevel = logger.settings.minLevel;

setLogLevel('notavalidlevel');

expect(logger.settings.minLevel).toBe(beforeLevel);
});

it('ignores empty string — leaves minLevel unchanged', () => {
setLogLevel('warn');
const beforeLevel = logger.settings.minLevel;

setLogLevel('');

expect(logger.settings.minLevel).toBe(beforeLevel);
});
});

// ── getLogLevel ─────────────────────────────────────────────────────────────

describe('getLogLevel', () => {
it('returns the string name for each known numeric level', () => {
for (const [levelName, levelValue] of Object.entries(LOG_LEVELS)) {
logger.settings.minLevel = levelValue;
expect(getLogLevel()).toBe(levelName);
}
});

it('defaults to "debug" when minLevel does not match any known level', () => {
// Use a numeric value that is not in LOG_LEVELS
logger.settings.minLevel = 999;
expect(getLogLevel()).toBe('debug');
});

it('round-trips correctly after setLogLevel', () => {
setLogLevel('error');
expect(getLogLevel()).toBe('error');

setLogLevel('silly');
expect(getLogLevel()).toBe('silly');
});
});
});
Loading