test(unit): migrate batch1 core contracts to vitest

This commit is contained in:
Richard R 2026-05-30 11:11:44 -06:00
parent b16328abe9
commit 7570181b8a
14 changed files with 433 additions and 256 deletions

View file

@ -1,119 +0,0 @@
import { test, expect } from '@playwright/test';
import { isAnonymousAuthSessionsEnabled, isGithubAuthEnabled } from '../../src/lib/server/auth/config';
const ORIGINAL_BASE_URL = process.env.BASE_URL;
const ORIGINAL_AUTH_SECRET = process.env.AUTH_SECRET;
const ORIGINAL_USE_ANON = process.env.USE_ANONYMOUS_AUTH_SESSIONS;
const ORIGINAL_GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const ORIGINAL_GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
function restoreEnv() {
if (ORIGINAL_BASE_URL === undefined) delete process.env.BASE_URL;
else process.env.BASE_URL = ORIGINAL_BASE_URL;
if (ORIGINAL_AUTH_SECRET === undefined) delete process.env.AUTH_SECRET;
else process.env.AUTH_SECRET = ORIGINAL_AUTH_SECRET;
if (ORIGINAL_USE_ANON === undefined) delete process.env.USE_ANONYMOUS_AUTH_SESSIONS;
else process.env.USE_ANONYMOUS_AUTH_SESSIONS = ORIGINAL_USE_ANON;
if (ORIGINAL_GITHUB_CLIENT_ID === undefined) delete process.env.GITHUB_CLIENT_ID;
else process.env.GITHUB_CLIENT_ID = ORIGINAL_GITHUB_CLIENT_ID;
if (ORIGINAL_GITHUB_CLIENT_SECRET === undefined) delete process.env.GITHUB_CLIENT_SECRET;
else process.env.GITHUB_CLIENT_SECRET = ORIGINAL_GITHUB_CLIENT_SECRET;
}
function setAuthEnabledEnv() {
process.env.BASE_URL = 'http://localhost:3003';
process.env.AUTH_SECRET = 'test-secret';
}
test.describe('auth config anonymous-session flag', () => {
test.afterEach(() => {
restoreEnv();
});
test('returns false when auth is disabled', () => {
delete process.env.BASE_URL;
delete process.env.AUTH_SECRET;
process.env.USE_ANONYMOUS_AUTH_SESSIONS = 'true';
expect(isAnonymousAuthSessionsEnabled()).toBe(false);
});
test('defaults to false when env var is unset', () => {
setAuthEnabledEnv();
delete process.env.USE_ANONYMOUS_AUTH_SESSIONS;
expect(isAnonymousAuthSessionsEnabled()).toBe(false);
});
test('returns true only when env var is true', () => {
setAuthEnabledEnv();
process.env.USE_ANONYMOUS_AUTH_SESSIONS = 'true';
expect(isAnonymousAuthSessionsEnabled()).toBe(true);
});
test('returns false when env var is false', () => {
setAuthEnabledEnv();
process.env.USE_ANONYMOUS_AUTH_SESSIONS = 'false';
expect(isAnonymousAuthSessionsEnabled()).toBe(false);
});
test('falls back to false for invalid values', () => {
setAuthEnabledEnv();
process.env.USE_ANONYMOUS_AUTH_SESSIONS = '1';
expect(isAnonymousAuthSessionsEnabled()).toBe(false);
});
});
test.describe('auth config github-auth flag', () => {
test.afterEach(() => {
restoreEnv();
});
test('returns false when auth is disabled', () => {
delete process.env.BASE_URL;
delete process.env.AUTH_SECRET;
process.env.GITHUB_CLIENT_ID = 'some-id';
process.env.GITHUB_CLIENT_SECRET = 'some-secret';
expect(isGithubAuthEnabled()).toBe(false);
});
test('returns false when GITHUB_CLIENT_ID is missing', () => {
setAuthEnabledEnv();
delete process.env.GITHUB_CLIENT_ID;
process.env.GITHUB_CLIENT_SECRET = 'some-secret';
expect(isGithubAuthEnabled()).toBe(false);
});
test('returns false when GITHUB_CLIENT_SECRET is missing', () => {
setAuthEnabledEnv();
process.env.GITHUB_CLIENT_ID = 'some-id';
delete process.env.GITHUB_CLIENT_SECRET;
expect(isGithubAuthEnabled()).toBe(false);
});
test('returns false when both GitHub env vars are missing', () => {
setAuthEnabledEnv();
delete process.env.GITHUB_CLIENT_ID;
delete process.env.GITHUB_CLIENT_SECRET;
expect(isGithubAuthEnabled()).toBe(false);
});
test('returns true when auth is enabled and both GitHub env vars are set', () => {
setAuthEnabledEnv();
process.env.GITHUB_CLIENT_ID = 'some-id';
process.env.GITHUB_CLIENT_SECRET = 'some-secret';
expect(isGithubAuthEnabled()).toBe(true);
});
});

View file

@ -0,0 +1,111 @@
import { describe, expect, test } from 'vitest';
import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled, isGithubAuthEnabled } from '../../src/lib/server/auth/config';
import { withEnv } from './support/env';
describe('auth config contract', () => {
test('auth is enabled only when AUTH_SECRET and BASE_URL are both set', async () => {
await withEnv(
{
AUTH_SECRET: undefined,
BASE_URL: undefined,
},
async () => {
expect(isAuthEnabled()).toBe(false);
expect(getAuthBaseUrl()).toBeNull();
},
);
await withEnv(
{
AUTH_SECRET: 'unit-secret',
BASE_URL: 'http://localhost:3003',
},
async () => {
expect(isAuthEnabled()).toBe(true);
expect(getAuthBaseUrl()).toBe('http://localhost:3003');
},
);
});
test.each([
{ envValue: undefined, expected: false },
{ envValue: 'true', expected: true },
{ envValue: 'false', expected: false },
{ envValue: '1', expected: false },
{ envValue: 'TRUE', expected: true },
])(
'anonymous sessions honor strict boolean parsing when auth is enabled (value: $envValue)',
async ({ envValue, expected }) => {
await withEnv(
{
AUTH_SECRET: 'unit-secret',
BASE_URL: 'http://localhost:3003',
USE_ANONYMOUS_AUTH_SESSIONS: envValue,
},
async () => {
expect(isAnonymousAuthSessionsEnabled()).toBe(expected);
},
);
},
);
test('anonymous sessions are always disabled when auth is disabled', async () => {
await withEnv(
{
AUTH_SECRET: undefined,
BASE_URL: undefined,
USE_ANONYMOUS_AUTH_SESSIONS: 'true',
},
async () => {
expect(isAnonymousAuthSessionsEnabled()).toBe(false);
},
);
});
test.each([
{
title: 'returns false when auth is disabled',
env: {
AUTH_SECRET: undefined,
BASE_URL: undefined,
GITHUB_CLIENT_ID: 'id',
GITHUB_CLIENT_SECRET: 'secret',
},
expected: false,
},
{
title: 'returns false when GitHub client id is missing',
env: {
AUTH_SECRET: 'unit-secret',
BASE_URL: 'http://localhost:3003',
GITHUB_CLIENT_ID: undefined,
GITHUB_CLIENT_SECRET: 'secret',
},
expected: false,
},
{
title: 'returns false when GitHub client secret is missing',
env: {
AUTH_SECRET: 'unit-secret',
BASE_URL: 'http://localhost:3003',
GITHUB_CLIENT_ID: 'id',
GITHUB_CLIENT_SECRET: undefined,
},
expected: false,
},
{
title: 'returns true when auth is enabled and GitHub credentials are present',
env: {
AUTH_SECRET: 'unit-secret',
BASE_URL: 'http://localhost:3003',
GITHUB_CLIENT_ID: 'id',
GITHUB_CLIENT_SECRET: 'secret',
},
expected: true,
},
])('$title', async ({ env, expected }) => {
await withEnv(env, async () => {
expect(isGithubAuthEnabled()).toBe(expected);
});
});
});

View file

@ -1,8 +1,8 @@
import { expect, test } from '@playwright/test';
import { describe, expect, test } from 'vitest';
import { RUNTIME_CONFIG_SCHEMA } from '../../src/lib/server/admin/settings';
test.describe('TTS rate limit runtime config seeds', () => {
describe('TTS rate limit runtime config seeds', () => {
test('defaults disable TTS daily rate limiting', () => {
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.default).toBe(true);
});
@ -12,6 +12,9 @@ test.describe('TTS rate limit runtime config seeds', () => {
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.parseEnv('false')).toBe(false);
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.parseEnv('1')).toBe(true);
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.parseEnv('0')).toBe(false);
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.parseEnv('on')).toBe(true);
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.parseEnv('no')).toBe(false);
expect(RUNTIME_CONFIG_SCHEMA.disableTtsRateLimit.parseEnv('invalid')).toBeUndefined();
});
test('daily limit values are runtime-only (no env seed vars)', () => {

View file

@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test';
import { describe, expect, test } from 'vitest';
import {
ServerAppError,
createServerAppError,
@ -7,10 +7,12 @@ import {
toApiErrorBody,
toHttpStatus,
} from '../../src/lib/server/errors/contract';
import { makeServerAppErrorInput } from './support/factories';
test.describe('server error contract', () => {
describe('server error contract', () => {
test('normalizes unknown throwable to fallback shape', () => {
const normalized = normalizeServerError('boom');
expect(normalized.code).toBe('UNKNOWN_SERVER_ERROR');
expect(normalized.errorClass).toBe('unknown');
expect(normalized.httpStatus).toBe(500);
@ -19,15 +21,17 @@ test.describe('server error contract', () => {
});
test('preserves ServerAppError metadata', () => {
const err = createServerAppError({
const err = createServerAppError(makeServerAppErrorInput({
code: 'USER_PROGRESS_UPDATE_FAILED',
message: 'Failed to update progress',
errorClass: 'db',
retryable: true,
httpStatus: 500,
details: { operation: 'update_progress' },
});
}));
const normalized = normalizeServerError(err);
expect(isServerAppError(err)).toBe(true);
expect(normalized.code).toBe('USER_PROGRESS_UPDATE_FAILED');
expect(normalized.errorClass).toBe('db');
@ -36,6 +40,22 @@ test.describe('server error contract', () => {
expect(normalized.details?.operation).toBe('update_progress');
});
test('normalizes partially-shaped thrown errors with code sanitization', () => {
const thrown = Object.assign(new Error('Timed out waiting for storage read'), {
code: 'not_valid_code',
errorClass: 'timeout',
httpStatus: 604,
retryable: true,
});
const normalized = normalizeServerError(thrown);
expect(normalized.code).toBe('UNKNOWN_SERVER_ERROR');
expect(normalized.errorClass).toBe('timeout');
expect(normalized.httpStatus).toBe(599);
expect(normalized.retryable).toBe(true);
});
test('maps normalized errors to API body + status', () => {
const normalized = normalizeServerError(
new ServerAppError({
@ -44,7 +64,9 @@ test.describe('server error contract', () => {
errorClass: 'upstream',
}),
);
const body = toApiErrorBody(normalized, { includeDetails: false });
expect(body).toEqual({
error: 'Upstream failure',
errorCode: 'UPSTREAM_TTS_ERROR',
@ -52,4 +74,12 @@ test.describe('server error contract', () => {
});
expect(toHttpStatus(normalized)).toBe(502);
});
test('rejects invalid server error code at creation boundary', () => {
expect(() => createServerAppError(makeServerAppErrorInput({
code: 'bad-code',
message: 'Invalid',
errorClass: 'validation',
}))).toThrow(/Invalid ServerAppError code/i);
});
});

View file

@ -1,43 +0,0 @@
import { expect, test } from '@playwright/test';
import { errorResponse } from '../../src/lib/server/errors/next-response';
import { createServerAppError } from '../../src/lib/server/errors/contract';
test.describe('server error response helper', () => {
test('returns mapped 4xx response with explicit app code', async () => {
const response = errorResponse(
createServerAppError({
code: 'AUTH_UNAUTHORIZED',
message: 'Unauthorized',
errorClass: 'auth',
httpStatus: 401,
retryable: false,
}),
{
apiErrorMessage: 'Unauthorized',
},
);
expect(response.status).toBe(401);
await expect(response.json()).resolves.toEqual({
error: 'Unauthorized',
errorCode: 'AUTH_UNAUTHORIZED',
retryable: false,
});
});
test('normalizes unknown errors to safe 500 without stack leakage', async () => {
const response = errorResponse(new Error('sensitive internal details'), {
apiErrorMessage: 'Internal Server Error',
normalize: { code: 'UNKNOWN_SERVER_ERROR', errorClass: 'unknown' },
});
const body = await response.json();
expect(response.status).toBe(500);
expect(body).toEqual({
error: 'Internal Server Error',
errorCode: 'UNKNOWN_SERVER_ERROR',
retryable: false,
});
expect(JSON.stringify(body)).not.toContain('stack');
expect(JSON.stringify(body)).not.toContain('cause');
});
});

View file

@ -0,0 +1,90 @@
import { describe, expect, test } from 'vitest';
import { createServerAppError } from '../../src/lib/server/errors/contract';
import { asRouteErrorContext, errorResponse, withErrorBoundary } from '../../src/lib/server/errors/next-response';
import { createServerLoggerStub } from './support/server-stubs';
import { makeServerAppErrorInput } from './support/factories';
describe('server error response helper', () => {
test('returns mapped 4xx response with explicit app code', async () => {
const response = errorResponse(
createServerAppError(makeServerAppErrorInput({
code: 'AUTH_UNAUTHORIZED',
message: 'Unauthorized',
errorClass: 'auth',
httpStatus: 401,
retryable: false,
})),
{
apiErrorMessage: 'Unauthorized',
},
);
expect(response.status).toBe(401);
await expect(response.json()).resolves.toEqual({
error: 'Unauthorized',
errorCode: 'AUTH_UNAUTHORIZED',
retryable: false,
});
});
test('normalizes unknown errors to safe 500 without stack leakage', async () => {
const response = errorResponse(new Error('sensitive internal details'), {
apiErrorMessage: 'Internal Server Error',
normalize: { code: 'UNKNOWN_SERVER_ERROR', errorClass: 'unknown' },
});
const body = await response.json();
expect(response.status).toBe(500);
expect(body).toEqual({
error: 'Internal Server Error',
errorCode: 'UNKNOWN_SERVER_ERROR',
retryable: false,
});
expect(JSON.stringify(body)).not.toContain('stack');
expect(JSON.stringify(body)).not.toContain('cause');
});
test('withErrorBoundary returns normalized error response and emits a structured log', async () => {
const logger = createServerLoggerStub();
const handler = withErrorBoundary(
async () => {
throw new Error('storage dependency timed out');
},
{
route: '/api/unit-test',
logger: logger as never,
event: 'unit_test.route.failed',
msg: 'Route failed',
normalize: {
code: 'DOCUMENT_BLOB_FETCH_FAILED',
errorClass: 'storage',
},
apiErrorMessage: 'Failed to fetch document blob',
},
);
const response = await handler();
const body = await response.json();
expect(response.status).toBe(503);
expect(body).toEqual({
error: 'Failed to fetch document blob',
errorCode: 'DOCUMENT_BLOB_FETCH_FAILED',
retryable: true,
});
expect(logger.error).toHaveBeenCalledTimes(1);
});
test('asRouteErrorContext derives method and request id consistently', () => {
const context = asRouteErrorContext({
request: { method: 'PATCH' } as never,
route: '/api/runtime-config',
requestId: 'req-123',
});
expect(context).toEqual({
route: '/api/runtime-config',
method: 'PATCH',
requestId: 'req-123',
});
});
});

View file

@ -1,78 +0,0 @@
import { expect, test } from '@playwright/test';
import { errorResponse } from '../../src/lib/server/errors/next-response';
import { AdminProviderError } from '../../src/lib/server/admin/providers';
test.describe('route error mapping contract', () => {
test('admin provider validation errors map to 4xx via route normalize policy', async () => {
const adminError = new AdminProviderError('slug is required', 400);
const response = errorResponse(adminError, {
apiErrorMessage: adminError.message,
normalize: {
code: 'ADMIN_PROVIDERS_CREATE_REQUEST_FAILED',
errorClass: 'validation',
httpStatus: adminError.status,
retryable: false,
},
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
error: 'slug is required',
errorCode: 'ADMIN_PROVIDERS_CREATE_REQUEST_FAILED',
retryable: false,
});
});
test('documents storage failures map to retryable 503', async () => {
const response = errorResponse(new Error('blobstore timeout'), {
apiErrorMessage: 'Failed to fetch document blob',
normalize: {
code: 'DOCUMENT_BLOB_FETCH_FAILED',
errorClass: 'storage',
},
});
expect(response.status).toBe(503);
await expect(response.json()).resolves.toEqual({
error: 'Failed to fetch document blob',
errorCode: 'DOCUMENT_BLOB_FETCH_FAILED',
retryable: true,
});
});
test('audiobook upstream failures map to retryable 502', async () => {
const response = errorResponse(new Error('provider overloaded'), {
apiErrorMessage: 'Failed to process audio chapter',
normalize: {
code: 'AUDIOBOOK_CHAPTER_PROCESS_FAILED',
errorClass: 'upstream',
},
});
expect(response.status).toBe(502);
await expect(response.json()).resolves.toEqual({
error: 'Failed to process audio chapter',
errorCode: 'AUDIOBOOK_CHAPTER_PROCESS_FAILED',
retryable: true,
});
});
test('user export auth initialization failure maps to 500 auth classification', async () => {
const response = errorResponse(new Error('Auth not initialized'), {
apiErrorMessage: 'Auth not initialized',
normalize: {
code: 'USER_EXPORT_AUTH_NOT_INITIALIZED',
errorClass: 'auth',
httpStatus: 500,
retryable: false,
},
});
expect(response.status).toBe(500);
await expect(response.json()).resolves.toEqual({
error: 'Auth not initialized',
errorCode: 'USER_EXPORT_AUTH_NOT_INITIALIZED',
retryable: false,
});
});
});

View file

@ -0,0 +1,80 @@
import { describe, expect, test } from 'vitest';
import { AdminProviderError } from '../../src/lib/server/admin/providers';
import { errorResponse } from '../../src/lib/server/errors/next-response';
import { makeServerErrorContext } from './support/factories';
describe('route error mapping contract', () => {
test('admin provider validation errors map to 4xx via route normalize policy', async () => {
const adminError = new AdminProviderError('slug is required', 400);
const response = errorResponse(adminError, {
apiErrorMessage: adminError.message,
normalize: makeServerErrorContext({
code: 'ADMIN_PROVIDERS_CREATE_REQUEST_FAILED',
errorClass: 'validation',
httpStatus: adminError.status,
retryable: false,
}),
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
error: 'slug is required',
errorCode: 'ADMIN_PROVIDERS_CREATE_REQUEST_FAILED',
retryable: false,
});
});
test.each([
{
name: 'documents storage failures map to retryable 503',
thrown: new Error('blobstore timeout'),
apiErrorMessage: 'Failed to fetch document blob',
normalize: makeServerErrorContext({
code: 'DOCUMENT_BLOB_FETCH_FAILED',
errorClass: 'storage',
}),
expectedStatus: 503,
expectedBody: {
error: 'Failed to fetch document blob',
errorCode: 'DOCUMENT_BLOB_FETCH_FAILED',
retryable: true,
},
},
{
name: 'audiobook upstream failures map to retryable 502',
thrown: new Error('provider overloaded'),
apiErrorMessage: 'Failed to process audio chapter',
normalize: makeServerErrorContext({
code: 'AUDIOBOOK_CHAPTER_PROCESS_FAILED',
errorClass: 'upstream',
}),
expectedStatus: 502,
expectedBody: {
error: 'Failed to process audio chapter',
errorCode: 'AUDIOBOOK_CHAPTER_PROCESS_FAILED',
retryable: true,
},
},
{
name: 'user export auth initialization failure maps to 500 auth classification',
thrown: new Error('Auth not initialized'),
apiErrorMessage: 'Auth not initialized',
normalize: makeServerErrorContext({
code: 'USER_EXPORT_AUTH_NOT_INITIALIZED',
errorClass: 'auth',
httpStatus: 500,
retryable: false,
}),
expectedStatus: 500,
expectedBody: {
error: 'Auth not initialized',
errorCode: 'USER_EXPORT_AUTH_NOT_INITIALIZED',
retryable: false,
},
},
])('$name', async ({ thrown, apiErrorMessage, normalize, expectedStatus, expectedBody }) => {
const response = errorResponse(thrown, { apiErrorMessage, normalize });
expect(response.status).toBe(expectedStatus);
await expect(response.json()).resolves.toEqual(expectedBody);
});
});

View file

@ -1,14 +1,11 @@
import { expect, test } from '@playwright/test';
import { describe, expect, test } from 'vitest';
import { resolvePreferredSharedProviderSlug } from '../../src/lib/shared/shared-provider-selection';
import { makeSharedProviders } from './support/factories';
const PROVIDERS = [
{ slug: 'shared-a' },
{ slug: 'default-openai' },
{ slug: 'shared-b' },
] as const;
const PROVIDERS = makeSharedProviders(['shared-a', 'default-openai', 'shared-b']);
test.describe('resolvePreferredSharedProviderSlug', () => {
describe('resolvePreferredSharedProviderSlug', () => {
test('prefers requested shared slug when present', () => {
expect(resolvePreferredSharedProviderSlug({
providers: PROVIDERS,

View file

@ -1,9 +1,9 @@
import { expect, test } from '@playwright/test';
import { describe, expect, test } from 'vitest';
import { RUNTIME_CONFIG_SCHEMA } from '../../src/lib/server/admin/settings';
import { assertUserSignupAllowed } from '../../src/lib/server/auth/signup-policy';
test.describe('enableUserSignups runtime config', () => {
describe('enableUserSignups runtime config', () => {
test('defaults to enabled', () => {
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.default).toBe(true);
});
@ -13,10 +13,13 @@ test.describe('enableUserSignups runtime config', () => {
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.parseEnv('false')).toBe(false);
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.parseEnv('1')).toBe(true);
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.parseEnv('0')).toBe(false);
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.parseEnv('yes')).toBe(true);
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.parseEnv('off')).toBe(false);
expect(RUNTIME_CONFIG_SCHEMA.enableUserSignups.parseEnv('maybe')).toBeUndefined();
});
});
test.describe('signup policy enforcement', () => {
describe('signup policy enforcement', () => {
test('allows new non-anonymous users when signups are enabled', () => {
expect(() => assertUserSignupAllowed({ enableUserSignups: true, isAnonymous: false })).not.toThrow();
});

30
tests/unit/support/env.ts Normal file
View file

@ -0,0 +1,30 @@
export type EnvPatch = Record<string, string | undefined>;
export function captureEnv(keys: readonly string[]): EnvPatch {
const snapshot: EnvPatch = {};
for (const key of keys) {
snapshot[key] = process.env[key];
}
return snapshot;
}
export function restoreEnv(snapshot: EnvPatch): void {
for (const [key, value] of Object.entries(snapshot)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
export async function withEnv<T>(patch: EnvPatch, run: () => T | Promise<T>): Promise<T> {
const keys = Object.keys(patch);
const snapshot = captureEnv(keys);
for (const [key, value] of Object.entries(patch)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
try {
return await run();
} finally {
restoreEnv(snapshot);
}
}

View file

@ -0,0 +1,30 @@
import type { ServerErrorClass, ServerErrorContext } from '../../../src/lib/server/errors/contract';
export function makeServerAppErrorInput(overrides: Partial<{
code: string;
message: string;
errorClass: ServerErrorClass;
httpStatus: number;
retryable: boolean;
details: Record<string, unknown>;
cause: unknown;
}> = {}) {
return {
code: 'UNKNOWN_SERVER_ERROR',
message: 'Unhandled server failure',
errorClass: 'unknown' as const,
...overrides,
};
}
export function makeServerErrorContext(overrides: ServerErrorContext = {}): ServerErrorContext {
return {
code: 'UNKNOWN_SERVER_ERROR',
errorClass: 'unknown',
...overrides,
};
}
export function makeSharedProviders(slugs: readonly string[]): Array<{ slug: string }> {
return slugs.map((slug) => ({ slug }));
}

View file

@ -0,0 +1,22 @@
import type { Mock } from 'vitest';
import { vi } from 'vitest';
export interface ServerLoggerStub {
child: Mock;
debug: Mock;
error: Mock;
info: Mock;
warn: Mock;
}
export function createServerLoggerStub(): ServerLoggerStub {
const logger = {
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
child: vi.fn(),
} as ServerLoggerStub;
logger.child.mockReturnValue(logger);
return logger;
}

View file

@ -1,9 +1,30 @@
import { defineConfig } from 'vitest/config';
import { fileURLToPath } from 'node:url';
const srcDir = fileURLToPath(new URL('./src/', import.meta.url));
const alias = [
{ find: /^@\//, replacement: `${srcDir}` },
{ find: '@', replacement: srcDir },
];
export default defineConfig({
resolve: {
alias,
},
test: {
alias,
reporters: process.env.CI ? ['default', 'github'] : ['default'],
projects: [
{
resolve: {
alias,
},
test: {
name: 'openreader',
environment: 'node',
include: ['tests/unit/**/*.vitest.spec.ts'],
},
},
{
test: {
name: 'compute-core',