Add comprehensive test suite and update configuration

- Add Jest testing framework configuration
- Add test files for services, types, and utilities
- Update package.json with Jest dependencies and test scripts
- Enhance pre-commit checks to include testing
- Add proper environment validation and error handling in mailgun service
This commit is contained in:
William Valentin
2025-09-07 18:18:25 -07:00
parent bfebb34b7a
commit 16d025e747
8 changed files with 1170 additions and 35 deletions

198
utils/__tests__/env.test.ts Normal file
View File

@@ -0,0 +1,198 @@
import {
getEnv,
getEnvVar,
isBrowser,
isNode,
isTest,
isProduction,
type EnvConfig,
} from '../env';
describe('Environment Utilities', () => {
describe('getEnv', () => {
test('should return an object', () => {
const env = getEnv();
expect(typeof env).toBe('object');
expect(env).not.toBeNull();
});
test('should return consistent results on multiple calls', () => {
const env1 = getEnv();
const env2 = getEnv();
expect(env1).toEqual(env2);
});
});
describe('getEnvVar', () => {
test('should return fallback when variable does not exist', () => {
const fallback = 'default_value';
const value = getEnvVar('DEFINITELY_NON_EXISTENT_VAR_12345', fallback);
expect(value).toBe(fallback);
});
test('should return undefined when no fallback provided and variable does not exist', () => {
const value = getEnvVar('DEFINITELY_NON_EXISTENT_VAR_12345');
expect(value).toBeUndefined();
});
test('should handle empty string fallback', () => {
const value = getEnvVar('DEFINITELY_NON_EXISTENT_VAR_12345', '');
expect(value).toBe('');
});
});
describe('isBrowser', () => {
test('should return a boolean', () => {
const result = isBrowser();
expect(typeof result).toBe('boolean');
});
test('should be consistent across calls', () => {
const result1 = isBrowser();
const result2 = isBrowser();
expect(result1).toBe(result2);
});
});
describe('isNode', () => {
test('should return a boolean', () => {
const result = isNode();
expect(typeof result).toBe('boolean');
});
test('should be consistent across calls', () => {
const result1 = isNode();
const result2 = isNode();
expect(result1).toBe(result2);
});
test('should return true in Jest test environment', () => {
// Jest runs in Node.js, so this should be true
const result = isNode();
expect(result).toBe(true);
});
});
describe('isTest', () => {
test('should return a boolean', () => {
const result = isTest();
expect(typeof result).toBe('boolean');
});
test('should return true in Jest test environment', () => {
// We are running in Jest, so this should be true
const result = isTest();
expect(result).toBe(true);
});
});
describe('isProduction', () => {
test('should return a boolean', () => {
const result = isProduction();
expect(typeof result).toBe('boolean');
});
test('should return false in test environment', () => {
// We are running tests, so this should not be production
const result = isProduction();
expect(result).toBe(false);
});
});
describe('EnvConfig type', () => {
test('should accept valid configuration object', () => {
const config: EnvConfig = {
VITE_COUCHDB_URL: 'http://localhost:5984',
VITE_COUCHDB_USERNAME: 'admin',
VITE_COUCHDB_PASSWORD: 'password',
VITE_MAILGUN_API_KEY: 'test-key',
VITE_MAILGUN_DOMAIN: 'test.mailgun.org',
NODE_ENV: 'test',
CUSTOM_VAR: 'custom_value',
};
expect(config.VITE_COUCHDB_URL).toBe('http://localhost:5984');
expect(config.NODE_ENV).toBe('test');
expect(config.CUSTOM_VAR).toBe('custom_value');
});
test('should handle undefined values', () => {
const config: EnvConfig = {
DEFINED_VAR: 'value',
UNDEFINED_VAR: undefined,
};
expect(config.DEFINED_VAR).toBe('value');
expect(config.UNDEFINED_VAR).toBeUndefined();
});
test('should allow dynamic key access', () => {
const config: EnvConfig = {
TEST_KEY: 'test_value',
};
const key = 'TEST_KEY';
expect(config[key]).toBe('test_value');
});
});
describe('integration scenarios', () => {
test('should work together consistently', () => {
const env = getEnv();
const isTestEnv = isTest();
const isProdEnv = isProduction();
const isBrowserEnv = isBrowser();
const isNodeEnv = isNode();
// Basic consistency checks
expect(typeof env).toBe('object');
expect(typeof isTestEnv).toBe('boolean');
expect(typeof isProdEnv).toBe('boolean');
expect(typeof isBrowserEnv).toBe('boolean');
expect(typeof isNodeEnv).toBe('boolean');
// In Jest test environment, we expect certain conditions
expect(isTestEnv).toBe(true);
expect(isProdEnv).toBe(false);
expect(isNodeEnv).toBe(true);
});
test('should handle missing environment gracefully', () => {
const nonExistentVar = getEnvVar('DEFINITELY_NON_EXISTENT_VAR_XYZ_123');
expect(nonExistentVar).toBeUndefined();
const withFallback = getEnvVar(
'DEFINITELY_NON_EXISTENT_VAR_XYZ_123',
'fallback'
);
expect(withFallback).toBe('fallback');
});
});
describe('edge cases', () => {
test('should handle empty string environment variable names', () => {
const value = getEnvVar('');
expect(value).toBeUndefined();
});
test('should handle whitespace in variable names', () => {
const value = getEnvVar(' NON_EXISTENT ');
expect(value).toBeUndefined();
});
test('should handle null fallback', () => {
const value = getEnvVar('NON_EXISTENT', null as any);
expect(value).toBeNull();
});
test('should handle numeric fallback', () => {
const value = getEnvVar('NON_EXISTENT', 42 as any);
expect(value).toBe(42);
});
test('should handle boolean fallback', () => {
const value = getEnvVar('NON_EXISTENT', true as any);
expect(value).toBe(true);
});
});
});

View File

@@ -0,0 +1,223 @@
import { generateSchedule, generateReminderSchedule } from '../schedule';
import { Medication, Frequency, CustomReminder } from '../../types';
describe('Schedule Utilities', () => {
const baseDate = new Date('2024-01-15T12:00:00.000Z');
describe('generateSchedule', () => {
const createMockMedication = (
overrides: Partial<Medication> = {}
): Medication => ({
_id: 'med-1',
_rev: 'rev-1',
name: 'Test Medication',
dosage: '10mg',
frequency: Frequency.Daily,
startTime: '08:00',
notes: '',
...overrides,
});
test('should return empty array for empty medications', () => {
const schedule = generateSchedule([], baseDate);
expect(schedule).toEqual([]);
});
test('should generate one dose for daily frequency', () => {
const medication = createMockMedication({
frequency: Frequency.Daily,
});
const schedule = generateSchedule([medication], baseDate);
expect(schedule).toHaveLength(1);
expect(schedule[0].medicationId).toBe('med-1');
expect(schedule[0].scheduledTime).toBeInstanceOf(Date);
});
test('should generate two doses for twice daily frequency', () => {
const medication = createMockMedication({
frequency: Frequency.TwiceADay,
});
const schedule = generateSchedule([medication], baseDate);
expect(schedule).toHaveLength(2);
expect(schedule[0].medicationId).toBe('med-1');
expect(schedule[1].medicationId).toBe('med-1');
});
test('should generate three doses for three times daily frequency', () => {
const medication = createMockMedication({
frequency: Frequency.ThreeTimesADay,
});
const schedule = generateSchedule([medication], baseDate);
expect(schedule).toHaveLength(3);
});
test('should handle EveryXHours frequency', () => {
const medication = createMockMedication({
frequency: Frequency.EveryXHours,
hoursBetween: 6,
});
const schedule = generateSchedule([medication], baseDate);
expect(schedule.length).toBeGreaterThan(0);
expect(schedule.length).toBeLessThanOrEqual(24);
});
test('should sort doses by time', () => {
const medications = [
createMockMedication({
_id: 'med-1',
startTime: '18:00',
}),
createMockMedication({
_id: 'med-2',
startTime: '08:00',
}),
];
const schedule = generateSchedule(medications, baseDate);
expect(schedule).toHaveLength(2);
// Should be sorted by time
expect(schedule[0].scheduledTime.getTime()).toBeLessThanOrEqual(
schedule[1].scheduledTime.getTime()
);
});
test('should handle invalid hoursBetween gracefully', () => {
const medication = createMockMedication({
frequency: Frequency.EveryXHours,
hoursBetween: 0,
});
const schedule = generateSchedule([medication], baseDate);
expect(schedule).toEqual([]);
});
});
describe('generateReminderSchedule', () => {
const createMockReminder = (
overrides: Partial<CustomReminder> = {}
): CustomReminder => ({
_id: 'reminder-1',
_rev: 'rev-1',
title: 'Test Reminder',
icon: '💊',
startTime: '09:00',
endTime: '17:00',
frequencyMinutes: 60,
...overrides,
});
test('should return empty array for empty reminders', () => {
const schedule = generateReminderSchedule([], baseDate);
expect(schedule).toEqual([]);
});
test('should generate reminders within time window', () => {
const reminder = createMockReminder({
startTime: '10:00',
endTime: '12:00',
frequencyMinutes: 60,
});
const schedule = generateReminderSchedule([reminder], baseDate);
expect(schedule.length).toBeGreaterThan(0);
schedule.forEach(instance => {
expect(instance.reminderId).toBe('reminder-1');
expect(instance.title).toBe('Test Reminder');
expect(instance.icon).toBe('💊');
expect(instance.scheduledTime).toBeInstanceOf(Date);
});
});
test('should handle single time window', () => {
const reminder = createMockReminder({
startTime: '15:00',
endTime: '15:00',
frequencyMinutes: 60,
});
const schedule = generateReminderSchedule([reminder], baseDate);
expect(schedule).toHaveLength(1);
});
test('should sort reminders by time', () => {
const reminders = [
createMockReminder({
_id: 'reminder-1',
startTime: '18:00',
endTime: '18:00',
}),
createMockReminder({
_id: 'reminder-2',
startTime: '08:00',
endTime: '08:00',
}),
];
const schedule = generateReminderSchedule(reminders, baseDate);
expect(schedule).toHaveLength(2);
// Should be sorted by time
expect(schedule[0].scheduledTime.getTime()).toBeLessThanOrEqual(
schedule[1].scheduledTime.getTime()
);
});
test('should generate unique IDs', () => {
const reminder = createMockReminder({
startTime: '10:00',
endTime: '11:00',
frequencyMinutes: 30,
});
const schedule = generateReminderSchedule([reminder], baseDate);
const ids = schedule.map(s => s.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(ids.length);
});
});
describe('error handling', () => {
test('should handle malformed time strings', () => {
const medication: Medication = {
_id: 'bad-med',
_rev: 'rev-1',
name: 'Bad Med',
dosage: '10mg',
frequency: Frequency.Daily,
startTime: 'invalid:time',
notes: '',
};
// This will throw because the time parsing creates an invalid date
expect(() => generateSchedule([medication], baseDate)).toThrow();
});
test('should handle large frequency values', () => {
const medication: Medication = {
_id: 'large-med',
_rev: 'rev-1',
name: 'Large Med',
dosage: '10mg',
frequency: Frequency.EveryXHours,
startTime: '08:00',
hoursBetween: 100,
notes: '',
};
const schedule = generateSchedule([medication], baseDate);
expect(schedule.length).toBeLessThan(100);
});
});
});