mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:32:10 +01:00
feat: comprehensive security and architecture improvements
- Add Zod validation schemas with strong password requirements (12+ chars, complexity) - Implement rate limiting for authentication endpoints (registration, password reset) - Remove duplicate MetricCard component, consolidate to ui/metric-card.tsx - Update README.md to use pnpm commands consistently - Enhance authentication security with 12-round bcrypt hashing - Add comprehensive input validation for all API endpoints - Fix security vulnerabilities in user registration and password reset flows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
// Vitest test setup
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock console methods to reduce noise in tests
|
||||
global.console = {
|
||||
@ -10,8 +10,8 @@ global.console = {
|
||||
};
|
||||
|
||||
// Set test environment variables
|
||||
process.env.NEXTAUTH_SECRET = 'test-secret';
|
||||
process.env.NEXTAUTH_URL = 'http://localhost:3000';
|
||||
process.env.NEXTAUTH_SECRET = "test-secret";
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000";
|
||||
|
||||
// Use test database for all database operations during tests
|
||||
if (process.env.DATABASE_URL_TEST) {
|
||||
@ -19,6 +19,6 @@ if (process.env.DATABASE_URL_TEST) {
|
||||
}
|
||||
|
||||
// Mock node-fetch for transcript fetcher tests
|
||||
vi.mock('node-fetch', () => ({
|
||||
vi.mock("node-fetch", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
describe('Database Configuration', () => {
|
||||
describe("Database Configuration", () => {
|
||||
let prisma: PrismaClient;
|
||||
|
||||
beforeAll(() => {
|
||||
@ -12,62 +12,62 @@ describe('Database Configuration', () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
it('should connect to the test database', async () => {
|
||||
it("should connect to the test database", async () => {
|
||||
// Verify we can connect to the database
|
||||
const result = await prisma.$queryRaw`SELECT 1 as test`;
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use PostgreSQL as the database provider', async () => {
|
||||
it("should use PostgreSQL as the database provider", async () => {
|
||||
// Query the database to verify it's PostgreSQL
|
||||
const result = await prisma.$queryRaw`SELECT version()` as any[];
|
||||
expect(result[0].version).toContain('PostgreSQL');
|
||||
const result = (await prisma.$queryRaw`SELECT version()`) as any[];
|
||||
expect(result[0].version).toContain("PostgreSQL");
|
||||
});
|
||||
|
||||
it('should be using the test database URL', () => {
|
||||
it("should be using the test database URL", () => {
|
||||
// Verify that DATABASE_URL is set to the test database
|
||||
expect(process.env.DATABASE_URL).toBeDefined();
|
||||
expect(process.env.DATABASE_URL).toContain('postgresql://');
|
||||
|
||||
expect(process.env.DATABASE_URL).toContain("postgresql://");
|
||||
|
||||
// If DATABASE_URL_TEST is set, DATABASE_URL should match it (from our test setup)
|
||||
if (process.env.DATABASE_URL_TEST) {
|
||||
expect(process.env.DATABASE_URL).toBe(process.env.DATABASE_URL_TEST);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have all required tables', async () => {
|
||||
it("should have all required tables", async () => {
|
||||
// Verify all our tables exist
|
||||
const tables = await prisma.$queryRaw`
|
||||
const tables = (await prisma.$queryRaw`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
` as any[];
|
||||
`) as any[];
|
||||
|
||||
const tableNames = tables.map(t => t.table_name);
|
||||
|
||||
expect(tableNames).toContain('Company');
|
||||
expect(tableNames).toContain('User');
|
||||
expect(tableNames).toContain('Session');
|
||||
expect(tableNames).toContain('SessionImport');
|
||||
expect(tableNames).toContain('Message');
|
||||
expect(tableNames).toContain('Question');
|
||||
expect(tableNames).toContain('SessionQuestion');
|
||||
expect(tableNames).toContain('AIProcessingRequest');
|
||||
const tableNames = tables.map((t) => t.table_name);
|
||||
|
||||
expect(tableNames).toContain("Company");
|
||||
expect(tableNames).toContain("User");
|
||||
expect(tableNames).toContain("Session");
|
||||
expect(tableNames).toContain("SessionImport");
|
||||
expect(tableNames).toContain("Message");
|
||||
expect(tableNames).toContain("Question");
|
||||
expect(tableNames).toContain("SessionQuestion");
|
||||
expect(tableNames).toContain("AIProcessingRequest");
|
||||
});
|
||||
|
||||
it('should be able to create and query data', async () => {
|
||||
it("should be able to create and query data", async () => {
|
||||
// Test basic CRUD operations
|
||||
const company = await prisma.company.create({
|
||||
data: {
|
||||
name: 'Test Company',
|
||||
csvUrl: 'https://example.com/test.csv',
|
||||
name: "Test Company",
|
||||
csvUrl: "https://example.com/test.csv",
|
||||
},
|
||||
});
|
||||
|
||||
expect(company.id).toBeDefined();
|
||||
expect(company.name).toBe('Test Company');
|
||||
expect(company.name).toBe("Test Company");
|
||||
|
||||
// Clean up
|
||||
await prisma.company.delete({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Unit tests for environment management
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
describe('Environment Management', () => {
|
||||
describe("Environment Management", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -15,8 +15,8 @@ describe('Environment Management', () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe('env object', () => {
|
||||
it('should have default values when environment variables are not set', async () => {
|
||||
describe("env object", () => {
|
||||
it("should have default values when environment variables are not set", async () => {
|
||||
// Clear relevant env vars
|
||||
delete process.env.NEXTAUTH_URL;
|
||||
delete process.env.SCHEDULER_ENABLED;
|
||||
@ -24,151 +24,153 @@ describe('Environment Management', () => {
|
||||
|
||||
// Re-import to get fresh env object
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('http://localhost:3000');
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe("http://localhost:3000");
|
||||
// Note: SCHEDULER_ENABLED will be true because .env.local sets it to "true"
|
||||
expect(freshEnv.SCHEDULER_ENABLED).toBe(true);
|
||||
expect(freshEnv.PORT).toBe(3000);
|
||||
});
|
||||
|
||||
it('should use environment variables when set', async () => {
|
||||
process.env.NEXTAUTH_URL = 'https://example.com';
|
||||
process.env.SCHEDULER_ENABLED = 'true';
|
||||
process.env.PORT = '8080';
|
||||
it("should use environment variables when set", async () => {
|
||||
process.env.NEXTAUTH_URL = "https://example.com";
|
||||
process.env.SCHEDULER_ENABLED = "true";
|
||||
process.env.PORT = "8080";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('https://example.com');
|
||||
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', async () => {
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = '100';
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = '10';
|
||||
it("should parse numeric environment variables correctly", async () => {
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = "100";
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = "10";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(100);
|
||||
expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle invalid numeric values gracefully', async () => {
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = 'invalid';
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = '';
|
||||
it("should handle invalid numeric values gracefully", async () => {
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = "invalid";
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY = "";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(50); // Falls back to default value
|
||||
expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(5); // Falls back to default value
|
||||
});
|
||||
|
||||
it('should parse quoted environment variables correctly', async () => {
|
||||
it("should parse quoted environment variables correctly", async () => {
|
||||
process.env.NEXTAUTH_URL = '"https://quoted.example.com"';
|
||||
process.env.NEXTAUTH_SECRET = "'single-quoted-secret'";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('https://quoted.example.com');
|
||||
expect(freshEnv.NEXTAUTH_SECRET).toBe('single-quoted-secret');
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe("https://quoted.example.com");
|
||||
expect(freshEnv.NEXTAUTH_SECRET).toBe("single-quoted-secret");
|
||||
});
|
||||
|
||||
it('should strip inline comments from environment variables', async () => {
|
||||
process.env.CSV_IMPORT_INTERVAL = '*/10 * * * * # Custom comment';
|
||||
process.env.IMPORT_PROCESSING_INTERVAL = '*/3 * * * * # Another comment';
|
||||
it("should strip inline comments from environment variables", async () => {
|
||||
process.env.CSV_IMPORT_INTERVAL = "*/10 * * * * # Custom comment";
|
||||
process.env.IMPORT_PROCESSING_INTERVAL =
|
||||
"*/3 * * * * # Another comment";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.CSV_IMPORT_INTERVAL).toBe('*/10 * * * *');
|
||||
expect(freshEnv.IMPORT_PROCESSING_INTERVAL).toBe('*/3 * * * *');
|
||||
expect(freshEnv.CSV_IMPORT_INTERVAL).toBe("*/10 * * * *");
|
||||
expect(freshEnv.IMPORT_PROCESSING_INTERVAL).toBe("*/3 * * * *");
|
||||
});
|
||||
|
||||
it('should handle whitespace around environment variables', async () => {
|
||||
process.env.NEXTAUTH_URL = ' https://spaced.example.com ';
|
||||
process.env.PORT = ' 8080 ';
|
||||
it("should handle whitespace around environment variables", async () => {
|
||||
process.env.NEXTAUTH_URL = " https://spaced.example.com ";
|
||||
process.env.PORT = " 8080 ";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('https://spaced.example.com');
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe("https://spaced.example.com");
|
||||
expect(freshEnv.PORT).toBe(8080);
|
||||
});
|
||||
|
||||
it('should handle complex combinations of quotes, comments, and whitespace', async () => {
|
||||
process.env.NEXTAUTH_URL = ' "https://complex.example.com" # Production URL';
|
||||
it("should handle complex combinations of quotes, comments, and whitespace", async () => {
|
||||
process.env.NEXTAUTH_URL =
|
||||
' "https://complex.example.com" # Production URL';
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE = " '100' # Batch size";
|
||||
|
||||
vi.resetModules();
|
||||
const { env: freshEnv } = await import('../../lib/env');
|
||||
const { env: freshEnv } = await import("../../lib/env");
|
||||
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe('https://complex.example.com');
|
||||
expect(freshEnv.NEXTAUTH_URL).toBe("https://complex.example.com");
|
||||
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEnv', () => {
|
||||
it('should return valid when all required variables are set', async () => {
|
||||
vi.stubEnv('NEXTAUTH_SECRET', 'test-secret');
|
||||
vi.stubEnv('OPENAI_API_KEY', 'test-key');
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
describe("validateEnv", () => {
|
||||
it("should return valid when all required variables are set", async () => {
|
||||
vi.stubEnv("NEXTAUTH_SECRET", "test-secret");
|
||||
vi.stubEnv("OPENAI_API_KEY", "test-key");
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
|
||||
vi.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = await import('../../lib/env');
|
||||
const { validateEnv: freshValidateEnv } = await import("../../lib/env");
|
||||
|
||||
const result = freshValidateEnv();
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return invalid when NEXTAUTH_SECRET is missing', async () => {
|
||||
it("should return invalid when NEXTAUTH_SECRET is missing", async () => {
|
||||
// Test the validation logic by checking what happens with the current environment
|
||||
// Since .env.local provides values, we'll test the validation function directly
|
||||
const { validateEnv } = await import('../../lib/env');
|
||||
|
||||
const { validateEnv } = await import("../../lib/env");
|
||||
|
||||
// Mock the env object to simulate missing NEXTAUTH_SECRET
|
||||
const originalEnv = process.env.NEXTAUTH_SECRET;
|
||||
delete process.env.NEXTAUTH_SECRET;
|
||||
|
||||
|
||||
vi.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = await import('../../lib/env');
|
||||
|
||||
const { validateEnv: freshValidateEnv } = await import("../../lib/env");
|
||||
|
||||
const result = freshValidateEnv();
|
||||
|
||||
|
||||
// Restore the original value
|
||||
if (originalEnv) {
|
||||
process.env.NEXTAUTH_SECRET = originalEnv;
|
||||
}
|
||||
|
||||
|
||||
// Since .env.local loads values, this test validates the current setup is working
|
||||
// We expect it to be valid because .env.local provides the secret
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should require OPENAI_API_KEY in production', async () => {
|
||||
it("should require OPENAI_API_KEY in production", async () => {
|
||||
// Test the validation logic with production environment
|
||||
// Since .env.local provides values, this test validates the current behavior
|
||||
const { validateEnv } = await import('../../lib/env');
|
||||
|
||||
const { validateEnv } = await import("../../lib/env");
|
||||
|
||||
const result = validateEnv();
|
||||
|
||||
|
||||
// Since .env.local provides both NEXTAUTH_SECRET and OPENAI_API_KEY,
|
||||
// and NODE_ENV is 'development' by default, this should be valid
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should not require OPENAI_API_KEY in development', async () => {
|
||||
vi.stubEnv('NEXTAUTH_SECRET', 'test-secret');
|
||||
vi.stubEnv('OPENAI_API_KEY', '');
|
||||
vi.stubEnv('NODE_ENV', 'development');
|
||||
it("should not require OPENAI_API_KEY in development", async () => {
|
||||
vi.stubEnv("NEXTAUTH_SECRET", "test-secret");
|
||||
vi.stubEnv("OPENAI_API_KEY", "");
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
|
||||
vi.resetModules();
|
||||
const { validateEnv: freshValidateEnv } = await import('../../lib/env');
|
||||
const { validateEnv: freshValidateEnv } = await import("../../lib/env");
|
||||
|
||||
const result = freshValidateEnv();
|
||||
expect(result.valid).toBe(true);
|
||||
@ -176,45 +178,49 @@ describe('Environment Management', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSchedulerConfig', () => {
|
||||
it('should return correct scheduler configuration', async () => {
|
||||
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';
|
||||
describe("getSchedulerConfig", () => {
|
||||
it("should return correct scheduler configuration", async () => {
|
||||
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";
|
||||
|
||||
vi.resetModules();
|
||||
const { getSchedulerConfig: freshGetSchedulerConfig } = await import('../../lib/env');
|
||||
const { getSchedulerConfig: freshGetSchedulerConfig } = await import(
|
||||
"../../lib/env"
|
||||
);
|
||||
|
||||
const config = freshGetSchedulerConfig();
|
||||
|
||||
expect(config.enabled).toBe(true);
|
||||
expect(config.csvImport.interval).toBe('*/10 * * * *');
|
||||
expect(config.importProcessing.interval).toBe('*/3 * * * *');
|
||||
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.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', async () => {
|
||||
it("should use defaults when environment variables are not set", async () => {
|
||||
delete process.env.SCHEDULER_ENABLED;
|
||||
delete process.env.CSV_IMPORT_INTERVAL;
|
||||
delete process.env.IMPORT_PROCESSING_INTERVAL;
|
||||
|
||||
vi.resetModules();
|
||||
const { getSchedulerConfig: freshGetSchedulerConfig } = await import('../../lib/env');
|
||||
const { getSchedulerConfig: freshGetSchedulerConfig } = await import(
|
||||
"../../lib/env"
|
||||
);
|
||||
|
||||
const config = freshGetSchedulerConfig();
|
||||
|
||||
// Note: SCHEDULER_ENABLED will be true because .env.local sets it to "true"
|
||||
expect(config.enabled).toBe(true);
|
||||
// The .env.local file is loaded and comments are now stripped, so we expect clean values
|
||||
expect(config.csvImport.interval).toBe('*/15 * * * *');
|
||||
expect(config.importProcessing.interval).toBe('*/5 * * * *');
|
||||
expect(config.csvImport.interval).toBe("*/15 * * * *");
|
||||
expect(config.importProcessing.interval).toBe("*/5 * * * *");
|
||||
expect(config.importProcessing.batchSize).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,222 +1,237 @@
|
||||
// Unit tests for transcript fetcher
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import fetch from 'node-fetch';
|
||||
import {
|
||||
fetchTranscriptContent,
|
||||
isValidTranscriptUrl,
|
||||
extractSessionIdFromTranscript
|
||||
} from '../../lib/transcriptFetcher';
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import fetch from "node-fetch";
|
||||
import {
|
||||
fetchTranscriptContent,
|
||||
isValidTranscriptUrl,
|
||||
extractSessionIdFromTranscript,
|
||||
} from "../../lib/transcriptFetcher";
|
||||
|
||||
// Mock node-fetch
|
||||
const mockFetch = fetch as any;
|
||||
|
||||
describe('Transcript Fetcher', () => {
|
||||
describe("Transcript Fetcher", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchTranscriptContent', () => {
|
||||
it('should successfully fetch transcript content', async () => {
|
||||
describe("fetchTranscriptContent", () => {
|
||||
it("should successfully fetch transcript content", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: vi.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: vi.fn().mockResolvedValue('Authenticated transcript'),
|
||||
text: vi.fn().mockResolvedValue("Session transcript content"),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent(
|
||||
'https://example.com/transcript',
|
||||
'user123',
|
||||
'pass456'
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
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',
|
||||
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',
|
||||
'Authorization': expectedAuth,
|
||||
"User-Agent": "LiveDash-Transcript-Fetcher/1.0",
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle HTTP errors', async () => {
|
||||
it("should handle authentication with username and password", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: vi.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',
|
||||
statusText: "Not Found",
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
const result = await fetchTranscriptContent(
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('HTTP 404: Not Found');
|
||||
expect(result.error).toBe("HTTP 404: Not Found");
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty transcript content', async () => {
|
||||
it("should handle empty transcript content", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: vi.fn().mockResolvedValue(' '),
|
||||
text: vi.fn().mockResolvedValue(" "),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
const result = await fetchTranscriptContent(
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Empty transcript content');
|
||||
expect(result.error).toBe("Empty transcript content");
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('ENOTFOUND example.com'));
|
||||
it("should handle network errors", async () => {
|
||||
mockFetch.mockRejectedValue(new Error("ENOTFOUND example.com"));
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
const result = await fetchTranscriptContent(
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Domain not found');
|
||||
expect(result.error).toBe("Domain not found");
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle connection refused errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
|
||||
it("should handle connection refused errors", async () => {
|
||||
mockFetch.mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
const result = await fetchTranscriptContent(
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Connection refused');
|
||||
expect(result.error).toBe("Connection refused");
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle timeout errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Request timeout'));
|
||||
it("should handle timeout errors", async () => {
|
||||
mockFetch.mockRejectedValue(new Error("Request timeout"));
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
const result = await fetchTranscriptContent(
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Request timeout');
|
||||
expect(result.error).toBe("Request timeout");
|
||||
expect(result.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty URL', async () => {
|
||||
const result = await fetchTranscriptContent('');
|
||||
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.error).toBe("No transcript URL provided");
|
||||
expect(result.content).toBeUndefined();
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trim whitespace from content', async () => {
|
||||
it("should trim whitespace from content", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: vi.fn().mockResolvedValue(' \n Session content \n '),
|
||||
text: vi.fn().mockResolvedValue(" \n Session content \n "),
|
||||
};
|
||||
mockFetch.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const result = await fetchTranscriptContent('https://example.com/transcript');
|
||||
const result = await fetchTranscriptContent(
|
||||
"https://example.com/transcript"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toBe('Session content');
|
||||
expect(result.content).toBe("Session content");
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidTranscriptUrl', () => {
|
||||
it('should validate HTTP URLs', () => {
|
||||
expect(isValidTranscriptUrl('http://example.com/transcript')).toBe(true);
|
||||
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 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);
|
||||
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);
|
||||
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...';
|
||||
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');
|
||||
expect(result).toBe("abc123def456");
|
||||
});
|
||||
|
||||
it('should extract session ID from sessionId pattern', () => {
|
||||
const content = 'sessionId: xyz789\nTranscript data...';
|
||||
it("should extract session ID from sessionId pattern", () => {
|
||||
const content = "sessionId: xyz789\nTranscript data...";
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('xyz789');
|
||||
expect(result).toBe("xyz789");
|
||||
});
|
||||
|
||||
it('should extract session ID from id pattern', () => {
|
||||
const content = 'id: session-12345678\nChat log...';
|
||||
it("should extract session ID from id pattern", () => {
|
||||
const content = "id: session-12345678\nChat log...";
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('session-12345678');
|
||||
expect(result).toBe("session-12345678");
|
||||
});
|
||||
|
||||
it('should extract session ID from first line', () => {
|
||||
const content = 'abc123def456\nUser: Hello\nBot: Hi there';
|
||||
it("should extract session ID from first line", () => {
|
||||
const content = "abc123def456\nUser: Hello\nBot: Hi there";
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('abc123def456');
|
||||
expect(result).toBe("abc123def456");
|
||||
});
|
||||
|
||||
it('should return null for content without session ID', () => {
|
||||
const content = 'User: Hello\nBot: Hi there\nUser: How are you?';
|
||||
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);
|
||||
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...';
|
||||
it("should handle case-insensitive patterns", () => {
|
||||
const content = "SESSION_ID: ABC123\nContent...";
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('ABC123');
|
||||
expect(result).toBe("ABC123");
|
||||
});
|
||||
|
||||
it('should extract the first matching pattern', () => {
|
||||
const content = 'session_id: first123\nid: second456\nMore content...';
|
||||
it("should extract the first matching pattern", () => {
|
||||
const content = "session_id: first123\nid: second456\nMore content...";
|
||||
const result = extractSessionIdFromTranscript(content);
|
||||
expect(result).toBe('first123');
|
||||
expect(result).toBe("first123");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user