mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:12:10 +01:00
feat: add rawTranscriptContent field to SessionImport model
feat: enhance server initialization with environment validation and import processing scheduler test: add Jest setup for unit tests and mock console methods test: implement unit tests for environment management and validation test: create unit tests for transcript fetcher functionality
This commit is contained in:
25
tests/setup.ts
Normal file
25
tests/setup.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// Jest test setup
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock console methods to reduce noise in tests
|
||||
global.console = {
|
||||
...console,
|
||||
log: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
// Set test environment variables
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: 'test',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
process.env.NEXTAUTH_SECRET = 'test-secret';
|
||||
process.env.NEXTAUTH_URL = 'http://localhost:3000';
|
||||
|
||||
// Mock node-fetch for transcript fetcher tests
|
||||
jest.mock('node-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
174
tests/unit/env.test.ts
Normal file
174
tests/unit/env.test.ts
Normal file
@ -0,0 +1,174 @@
|
||||
// Unit tests for environment management
|
||||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { env, validateEnv, getSchedulerConfig } from '../../lib/env';
|
||||
|
||||
describe('Environment Management', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original environment
|
||||
originalEnv = { ...process.env };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('env object', () => {
|
||||
it('should have default values when environment variables are not set', () => {
|
||||
// Clear relevant env vars
|
||||
delete process.env.NEXTAUTH_URL;
|
||||
delete process.env.SCHEDULER_ENABLED;
|
||||
delete process.env.PORT;
|
||||
|
||||
// Re-import to get fresh env object
|
||||
jest.resetModules();
|
||||
const { env: freshEnv } = require('../../lib/env');
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('http://localhost:3000');
|
||||
expect(freshEnv.SCHEDULER_ENABLED).toBe(false);
|
||||
expect(freshEnv.PORT).toBe(3000);
|
||||
});
|
||||
|
||||
it('should use environment variables when set', () => {
|
||||
process.env.NEXTAUTH_URL = 'https://example.com';
|
||||
process.env.SCHEDULER_ENABLED = 'true';
|
||||
process.env.PORT = '8080';
|
||||
|
||||
jest.resetModules();
|
||||
const { env: freshEnv } = require('../../lib/env');
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('https://example.com');
|
||||
expect(freshEnv.SCHEDULER_ENABLED).toBe(true);
|
||||
expect(freshEnv.PORT).toBe(8080);
|
||||
});
|
||||
|
||||
it('should parse numeric environment variables correctly', () => {
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = '100';
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = '10';
|
||||
|
||||
jest.resetModules();
|
||||
const { env: freshEnv } = require('../../lib/env');
|
||||
|
||||
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(100);
|
||||
expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle invalid numeric values gracefully', () => {
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = 'invalid';
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = '';
|
||||
|
||||
jest.resetModules();
|
||||
const { env: freshEnv } = require('../../lib/env');
|
||||
|
||||
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(50); // default
|
||||
expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(5); // default
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEnv', () => {
|
||||
it('should return valid when all required variables are set', () => {
|
||||
process.env.NEXTAUTH_SECRET = 'test-secret';
|
||||
process.env.OPENAI_API_KEY = 'test-key';
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: 'production',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = require('../../lib/env');
|
||||
|
||||
const result = freshValidateEnv();
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return invalid when NEXTAUTH_SECRET is missing', () => {
|
||||
delete process.env.NEXTAUTH_SECRET;
|
||||
|
||||
jest.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = require('../../lib/env');
|
||||
|
||||
const result = freshValidateEnv();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('NEXTAUTH_SECRET is required');
|
||||
});
|
||||
|
||||
it('should require OPENAI_API_KEY in production', () => {
|
||||
process.env.NEXTAUTH_SECRET = 'test-secret';
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: 'production',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = require('../../lib/env');
|
||||
|
||||
const result = freshValidateEnv();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('OPENAI_API_KEY is required in production');
|
||||
});
|
||||
|
||||
it('should not require OPENAI_API_KEY in development', () => {
|
||||
process.env.NEXTAUTH_SECRET = 'test-secret';
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: 'development',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = require('../../lib/env');
|
||||
|
||||
const result = freshValidateEnv();
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSchedulerConfig', () => {
|
||||
it('should return correct scheduler configuration', () => {
|
||||
process.env.SCHEDULER_ENABLED = 'true';
|
||||
process.env.CSV_IMPORT_INTERVAL = '*/10 * * * *';
|
||||
process.env.IMPORT_PROCESSING_INTERVAL = '*/3 * * * *';
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = '25';
|
||||
process.env.SESSION_PROCESSING_INTERVAL = '0 2 * * *';
|
||||
process.env.SESSION_PROCESSING_BATCH_SIZE = '100';
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = '8';
|
||||
|
||||
jest.resetModules();
|
||||
const { getSchedulerConfig: freshGetSchedulerConfig } = require('../../lib/env');
|
||||
|
||||
const config = freshGetSchedulerConfig();
|
||||
|
||||
expect(config.enabled).toBe(true);
|
||||
expect(config.csvImport.interval).toBe('*/10 * * * *');
|
||||
expect(config.importProcessing.interval).toBe('*/3 * * * *');
|
||||
expect(config.importProcessing.batchSize).toBe(25);
|
||||
expect(config.sessionProcessing.interval).toBe('0 2 * * *');
|
||||
expect(config.sessionProcessing.batchSize).toBe(100);
|
||||
expect(config.sessionProcessing.concurrency).toBe(8);
|
||||
});
|
||||
|
||||
it('should use defaults when environment variables are not set', () => {
|
||||
delete process.env.SCHEDULER_ENABLED;
|
||||
delete process.env.CSV_IMPORT_INTERVAL;
|
||||
delete process.env.IMPORT_PROCESSING_INTERVAL;
|
||||
|
||||
jest.resetModules();
|
||||
const { getSchedulerConfig: freshGetSchedulerConfig } = require('../../lib/env');
|
||||
|
||||
const config = freshGetSchedulerConfig();
|
||||
|
||||
expect(config.enabled).toBe(false);
|
||||
expect(config.csvImport.interval).toBe('*/15 * * * *');
|
||||
expect(config.importProcessing.interval).toBe('*/5 * * * *');
|
||||
expect(config.importProcessing.batchSize).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
222
tests/unit/transcriptFetcher.test.ts
Normal file
222
tests/unit/transcriptFetcher.test.ts
Normal file
@ -0,0 +1,222 @@
|
||||
// Unit tests for transcript fetcher
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
import fetch from 'node-fetch';
|
||||
import {
|
||||
fetchTranscriptContent,
|
||||
isValidTranscriptUrl,
|
||||
extractSessionIdFromTranscript
|
||||
} from '../../lib/transcriptFetcher';
|
||||
|
||||
// Mock node-fetch
|
||||
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
||||
|
||||
describe('Transcript Fetcher', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchTranscriptContent', () => {
|
||||
it('should successfully fetch transcript content', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: jest.fn().mockResolvedValue('Session transcript content'),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toBe('Session transcript content');
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://example.com/transcript', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'LiveDash-Transcript-Fetcher/1.0',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle authentication with username and password', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: jest.fn().mockResolvedValue('Authenticated transcript'),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent(
|
||||
'https://example.com/transcript',
|
||||
'user123',
|
||||
'pass456'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toBe('Authenticated transcript');
|
||||
|
||||
const expectedAuth = 'Basic ' + Buffer.from('user123:pass456').toString('base64');
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://example.com/transcript', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'LiveDash-Transcript-Fetcher/1.0',
|
||||
'Authorization': expectedAuth,
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle HTTP errors', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('HTTP 404: Not Found');
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty transcript content', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: jest.fn().mockResolvedValue(' '),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Empty transcript content');
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('ENOTFOUND example.com'));
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Domain not found');
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle connection refused errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Connection refused');
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle timeout errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Request timeout'));
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Request timeout');
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty URL', async () => {
|
||||
const result = await fetchTranscriptContent('');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('No transcript URL provided');
|
||||
expect(result.content).toBeUndefined();
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trim whitespace from content', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: jest.fn().mockResolvedValue(' \n Session content \n '),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toBe('Session content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidTranscriptUrl', () => {
|
||||
it('should validate HTTP URLs', () => {
|
||||
expect(isValidTranscriptUrl('http://example.com/transcript')).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate HTTPS URLs', () => {
|
||||
expect(isValidTranscriptUrl('https://example.com/transcript')).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid URLs', () => {
|
||||
expect(isValidTranscriptUrl('not-a-url')).toBe(false);
|
||||
expect(isValidTranscriptUrl('ftp://example.com')).toBe(false);
|
||||
expect(isValidTranscriptUrl('')).toBe(false);
|
||||
expect(isValidTranscriptUrl(null as any)).toBe(false);
|
||||
expect(isValidTranscriptUrl(undefined as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle malformed URLs', () => {
|
||||
expect(isValidTranscriptUrl('http://')).toBe(false);
|
||||
expect(isValidTranscriptUrl('https://')).toBe(false);
|
||||
expect(isValidTranscriptUrl('://example.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractSessionIdFromTranscript', () => {
|
||||
it('should extract session ID from session_id pattern', () => {
|
||||
const content = 'session_id: abc123def456\nOther content...';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('abc123def456');
|
||||
});
|
||||
|
||||
it('should extract session ID from sessionId pattern', () => {
|
||||
const content = 'sessionId: xyz789\nTranscript data...';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('xyz789');
|
||||
});
|
||||
|
||||
it('should extract session ID from id pattern', () => {
|
||||
const content = 'id: session-12345678\nChat log...';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('session-12345678');
|
||||
});
|
||||
|
||||
it('should extract session ID from first line', () => {
|
||||
const content = 'abc123def456\nUser: Hello\nBot: Hi there';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('abc123def456');
|
||||
});
|
||||
|
||||
it('should return null for content without session ID', () => {
|
||||
const content = 'User: Hello\nBot: Hi there\nUser: How are you?';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for empty content', () => {
|
||||
expect(extractSessionIdFromTranscript('')).toBe(null);
|
||||
expect(extractSessionIdFromTranscript(null as any)).toBe(null);
|
||||
expect(extractSessionIdFromTranscript(undefined as any)).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle case-insensitive patterns', () => {
|
||||
const content = 'SESSION_ID: ABC123\nContent...';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('ABC123');
|
||||
});
|
||||
|
||||
it('should extract the first matching pattern', () => {
|
||||
const content = 'session_id: first123\nid: second456\nMore content...';
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('first123');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user