import { cn } from '@sim/emcn' import { createEnvMock } from '@sim/testing' import { formatDate, formatDateTime, formatDuration, formatTime, getTimezoneAbbreviation, } from '@sim/utils/formatting' import { afterEach, describe, expect, it, vi } from 'vitest' import { getRotatingApiKey } from '@/lib/core/config/api-keys' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { convertScheduleOptionsToCron } from '@/lib/core/utils/scheduling' import { getInvalidCharacters, isValidName, validateName } from '@/lib/core/utils/validation' vi.mock('crypto', () => ({ createCipheriv: vi.fn().mockReturnValue({ update: vi.fn().mockReturnValue('encrypted-data'), final: vi.fn().mockReturnValue('final-data'), getAuthTag: vi.fn().mockReturnValue({ toString: vi.fn().mockReturnValue('auth-tag'), }), }), createDecipheriv: vi.fn().mockReturnValue({ update: vi.fn().mockReturnValue('decrypted-data'), final: vi.fn().mockReturnValue('final-data'), setAuthTag: vi.fn(), }), randomBytes: vi.fn().mockReturnValue({ toString: vi.fn().mockReturnValue('random-iv'), }), })) vi.mock('@/lib/core/config/env', () => createEnvMock({ ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', OPENAI_API_KEY_1: 'test-openai-key-1', OPENAI_API_KEY_2: 'test-openai-key-2', OPENAI_API_KEY_3: 'test-openai-key-3', ANTHROPIC_API_KEY_1: 'test-anthropic-key-1', ANTHROPIC_API_KEY_2: 'test-anthropic-key-2', ANTHROPIC_API_KEY_3: 'test-anthropic-key-3', GEMINI_API_KEY_1: 'test-gemini-key-1', GEMINI_API_KEY_2: 'test-gemini-key-2', GEMINI_API_KEY_3: 'test-gemini-key-3', XAI_API_KEY_1: 'test-xai-key-1', XAI_API_KEY_2: 'test-xai-key-2', XAI_API_KEY_3: 'test-xai-key-3', }) ) afterEach(() => { vi.clearAllMocks() }) describe('cn (class name utility)', () => { it.concurrent('should merge class names correctly', () => { const result = cn('class1', 'class2') expect(result).toBe('class1 class2') }) it.concurrent('should handle conditional classes', () => { const isActive = true const result = cn('base', isActive && 'active') expect(result).toBe('base active') }) it.concurrent('should handle falsy values', () => { const result = cn('base', false && 'hidden', null, undefined, 0, '') expect(result).toBe('base') }) it.concurrent('should handle arrays of class names', () => { const result = cn('base', ['class1', 'class2']) expect(result).toContain('base') expect(result).toContain('class1') expect(result).toContain('class2') }) }) describe('encryption and decryption', () => { it.concurrent('should encrypt secrets correctly', async () => { const result = await encryptSecret('my-secret') expect(result).toHaveProperty('encrypted') expect(result).toHaveProperty('iv') expect(result.encrypted).toContain('random-iv') expect(result.encrypted).toContain('encrypted-data') expect(result.encrypted).toContain('final-data') expect(result.encrypted).toContain('auth-tag') }) it.concurrent('should decrypt secrets correctly', async () => { const result = await decryptSecret('iv:encrypted:authTag') expect(result).toHaveProperty('decrypted') expect(result.decrypted).toBe('decrypted-datafinal-data') }) it.concurrent('should throw error for invalid decrypt format', async () => { await expect(decryptSecret('invalid-format')).rejects.toThrow('Invalid encrypted value format') }) }) describe('convertScheduleOptionsToCron', () => { it.concurrent('should convert minutes schedule to cron', () => { const result = convertScheduleOptionsToCron('minutes', { minutesInterval: '5' }) expect(result).toBe('*/5 * * * *') }) it.concurrent('should convert hourly schedule to cron', () => { const result = convertScheduleOptionsToCron('hourly', { hourlyMinute: '30' }) expect(result).toBe('30 * * * *') }) it.concurrent('should convert daily schedule to cron', () => { const result = convertScheduleOptionsToCron('daily', { dailyTime: '15:30' }) expect(result).toBe('15 30 * * *') }) it.concurrent('should convert weekly schedule to cron', () => { const result = convertScheduleOptionsToCron('weekly', { weeklyDay: 'MON', weeklyDayTime: '09:30', }) expect(result).toBe('09 30 * * 1') }) it.concurrent('should convert monthly schedule to cron', () => { const result = convertScheduleOptionsToCron('monthly', { monthlyDay: '15', monthlyTime: '12:00', }) expect(result).toBe('12 00 15 * *') }) it.concurrent('should use custom cron expression directly', () => { const customCron = '*/15 9-17 * * 1-5' const result = convertScheduleOptionsToCron('custom', { cronExpression: customCron }) expect(result).toBe(customCron) }) it.concurrent('should throw error for unsupported schedule type', () => { expect(() => convertScheduleOptionsToCron('invalid', {})).toThrow('Unsupported schedule type') }) it.concurrent('should use default values when options are not provided', () => { const result = convertScheduleOptionsToCron('daily', {}) expect(result).toBe('00 09 * * *') }) }) describe('date formatting functions', () => { it.concurrent('should format datetime correctly', () => { const date = new Date('2023-05-15T14:30:00') const result = formatDateTime(date) expect(result).toMatch(/May 15, 2023/) expect(result).toMatch(/2:30 PM|14:30/) }) it.concurrent('should format date correctly', () => { const date = new Date('2023-05-15T14:30:00') const result = formatDate(date) expect(result).toMatch(/May 15, 2023/) expect(result).not.toMatch(/2:30|14:30/) }) it.concurrent('should format time correctly', () => { const date = new Date('2023-05-15T14:30:00') const result = formatTime(date) expect(result).toMatch(/2:30 PM|14:30/) expect(result).not.toMatch(/2023|May/) }) }) describe('formatDuration', () => { it.concurrent('should format milliseconds correctly', () => { const result = formatDuration(500) expect(result).toBe('500ms') }) it.concurrent('should format seconds correctly', () => { const result = formatDuration(5000) expect(result).toBe('5s') }) it.concurrent('should format minutes and seconds correctly', () => { const result = formatDuration(125000) // 2m 5s expect(result).toBe('2m 5s') }) it.concurrent('should format hours, minutes correctly', () => { const result = formatDuration(3725000) // 1h 2m 5s expect(result).toBe('1h 2m') }) }) describe('getTimezoneAbbreviation', () => { it.concurrent('should return UTC for UTC timezone', () => { const result = getTimezoneAbbreviation('UTC') expect(result).toBe('UTC') }) it.concurrent('should return PST/PDT for Los Angeles timezone', () => { const winterDate = new Date('2023-01-15') // Standard time const summerDate = new Date('2023-07-15') // Daylight time const winterResult = getTimezoneAbbreviation('America/Los_Angeles', winterDate) const summerResult = getTimezoneAbbreviation('America/Los_Angeles', summerDate) expect(['PST', 'PDT']).toContain(winterResult) expect(['PST', 'PDT']).toContain(summerResult) }) it.concurrent('should return JST for Tokyo timezone (no DST)', () => { const winterDate = new Date('2023-01-15') const summerDate = new Date('2023-07-15') const winterResult = getTimezoneAbbreviation('Asia/Tokyo', winterDate) const summerResult = getTimezoneAbbreviation('Asia/Tokyo', summerDate) expect(winterResult).toBe('JST') expect(summerResult).toBe('JST') }) it.concurrent('should return full timezone name for unknown timezones', () => { const result = getTimezoneAbbreviation('Unknown/Timezone') expect(result).toBe('Unknown/Timezone') }) }) describe('validateName', () => { it.concurrent('should remove invalid characters', () => { const result = validateName('test@#$%name') expect(result).toBe('testname') }) it.concurrent('should keep valid characters', () => { const result = validateName('test_name_123') expect(result).toBe('test_name_123') }) it.concurrent('should keep spaces', () => { const result = validateName('test name') expect(result).toBe('test name') }) it.concurrent('should handle empty string', () => { const result = validateName('') expect(result).toBe('') }) it.concurrent('should handle string with only invalid characters', () => { const result = validateName('@#$%') expect(result).toBe('') }) it.concurrent('should handle mixed valid and invalid characters', () => { const result = validateName('my-workflow@2023!') expect(result).toBe('myworkflow2023') }) it.concurrent('should collapse multiple spaces into single spaces', () => { const result = validateName('test multiple spaces') expect(result).toBe('test multiple spaces') }) it.concurrent('should handle mixed whitespace and invalid characters', () => { const result = validateName('test@#$ name') expect(result).toBe('test name') }) }) describe('isValidName', () => { it.concurrent('should return true for valid names', () => { expect(isValidName('test_name')).toBe(true) expect(isValidName('test123')).toBe(true) expect(isValidName('test name')).toBe(true) expect(isValidName('TestName')).toBe(true) expect(isValidName('')).toBe(true) }) it.concurrent('should return false for invalid names', () => { expect(isValidName('test@name')).toBe(false) expect(isValidName('test-name')).toBe(false) expect(isValidName('test#name')).toBe(false) expect(isValidName('test$name')).toBe(false) expect(isValidName('test%name')).toBe(false) }) }) describe('getInvalidCharacters', () => { it.concurrent('should return empty array for valid names', () => { const result = getInvalidCharacters('test_name_123') expect(result).toEqual([]) }) it.concurrent('should return invalid characters', () => { const result = getInvalidCharacters('test@#$name') expect(result).toEqual(['@', '#', '$']) }) it.concurrent('should return unique invalid characters', () => { const result = getInvalidCharacters('test@@##name') expect(result).toEqual(['@', '#']) }) it.concurrent('should handle empty string', () => { const result = getInvalidCharacters('') expect(result).toEqual([]) }) it.concurrent('should handle string with only invalid characters', () => { const result = getInvalidCharacters('@#$%') expect(result).toEqual(['@', '#', '$', '%']) }) }) describe('getRotatingApiKey', () => { it.concurrent('should return OpenAI API key based on current minute', () => { const result = getRotatingApiKey('openai') expect(result).toMatch(/^test-openai-key-[1-3]$/) }) it.concurrent('should return Anthropic API key based on current minute', () => { const result = getRotatingApiKey('anthropic') expect(result).toMatch(/^test-anthropic-key-[1-3]$/) }) it.concurrent('should return Gemini API key based on current minute', () => { const result = getRotatingApiKey('gemini') expect(result).toMatch(/^test-gemini-key-[1-3]$/) }) it.concurrent('should return xAI API key based on current minute', () => { const result = getRotatingApiKey('xai') expect(result).toMatch(/^test-xai-key-[1-3]$/) }) it.concurrent('should throw error for unsupported provider', () => { expect(() => getRotatingApiKey('unsupported')).toThrow('No rotation implemented for provider') }) it.concurrent('should rotate keys based on minute modulo', () => { const result = getRotatingApiKey('openai') expect(['test-openai-key-1', 'test-openai-key-2', 'test-openai-key-3']).toContain(result) }) })