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:
@@ -4,6 +4,8 @@
|
||||
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
|
||||
"testMatch": [
|
||||
"<rootDir>/services/**/__tests__/**/*.test.ts",
|
||||
"<rootDir>/utils/**/__tests__/**/*.test.ts",
|
||||
"<rootDir>/types/**/__tests__/**/*.test.ts",
|
||||
"<rootDir>/tests/**/*.test.ts",
|
||||
"<rootDir>/tests/**/*.test.js"
|
||||
],
|
||||
@@ -12,6 +14,7 @@
|
||||
"components/**/*.tsx",
|
||||
"hooks/**/*.ts",
|
||||
"utils/**/*.ts",
|
||||
"types/**/*.ts",
|
||||
"!**/*.d.ts",
|
||||
"!**/__tests__/**"
|
||||
],
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:fast": "jest --testPathPatterns='(utils|types|services).*test\\.(ts|js)$' --passWithNoTests",
|
||||
"test:unit": "jest --testPathPatterns='(utils|types).*test\\.(ts|js)$'",
|
||||
"test:services": "jest --testPathPatterns='services.*test\\.(ts|js)$'",
|
||||
"test:integration": "jest tests/integration/production.test.js",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
|
||||
@@ -66,7 +66,10 @@ print_status "Starting parallel checks..."
|
||||
# 1. Prettier formatting (fast, runs on all relevant files)
|
||||
run_check "prettier" "bun run pre-commit"
|
||||
|
||||
# 2. ESLint on staged JS/TS files only
|
||||
# 2. Fast unit tests (utils, types, services) - very quick
|
||||
run_check "fast-tests" "bun run test:fast"
|
||||
|
||||
# 3. ESLint on staged JS/TS files only
|
||||
STAGED_JS_TS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|jsx|ts|tsx)$' || true)
|
||||
if [ -n "$STAGED_JS_TS_FILES" ]; then
|
||||
# Convert newlines to spaces for proper argument passing
|
||||
@@ -77,7 +80,7 @@ else
|
||||
echo "No JS/TS files to lint" > "$TEMP_DIR/eslint.out"
|
||||
fi
|
||||
|
||||
# 3. TypeScript type checking on staged files
|
||||
# 4. TypeScript type checking on staged files
|
||||
STAGED_TS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx)$' || true)
|
||||
if [ -n "$STAGED_TS_FILES" ]; then
|
||||
run_check "typecheck" "./scripts/type-check-staged.sh"
|
||||
@@ -86,7 +89,7 @@ else
|
||||
echo "No TypeScript files to check" > "$TEMP_DIR/typecheck.out"
|
||||
fi
|
||||
|
||||
# 4. Markdown linting on staged markdown files
|
||||
# 5. Markdown linting on staged markdown files
|
||||
STAGED_MD_FILES=$(echo "$STAGED_FILES" | grep -E '\.md$' || true)
|
||||
if [ -n "$STAGED_MD_FILES" ]; then
|
||||
# Convert newlines to spaces for proper argument passing
|
||||
@@ -97,7 +100,7 @@ else
|
||||
echo "No markdown files to lint" > "$TEMP_DIR/markdown.out"
|
||||
fi
|
||||
|
||||
# 5. Secret scanning on staged files (optional check)
|
||||
# 6. Secret scanning on staged files (optional check)
|
||||
if command -v secretlint > /dev/null; then
|
||||
# Convert newlines to spaces for proper argument passing
|
||||
SECRET_FILES=$(echo "$STAGED_FILES" | tr '\n' ' ')
|
||||
@@ -111,7 +114,7 @@ fi
|
||||
print_status "Waiting for checks to complete..."
|
||||
|
||||
FAILED_CHECKS=()
|
||||
ALL_CHECKS=("prettier" "eslint" "typecheck" "markdown" "secrets")
|
||||
ALL_CHECKS=("prettier" "fast-tests" "eslint" "typecheck" "markdown" "secrets")
|
||||
|
||||
for check in "${ALL_CHECKS[@]}"; do
|
||||
if [ -f "$TEMP_DIR/$check.pid" ]; then
|
||||
@@ -124,7 +127,7 @@ for check in "${ALL_CHECKS[@]}"; do
|
||||
print_success "$check passed"
|
||||
else
|
||||
# For some checks, failure is not critical
|
||||
if [ "$check" = "secrets" ] || [ "$check" = "markdown" ]; then
|
||||
if [ "$check" = "secrets" ] || [ "$check" = "markdown" ] || [ "$check" = "fast-tests" ]; then
|
||||
print_warning "$check had issues (non-critical)"
|
||||
if [ -f "$TEMP_DIR/$check.out" ] && [ -s "$TEMP_DIR/$check.out" ]; then
|
||||
echo -e "${YELLOW}$check output:${NC}"
|
||||
@@ -156,6 +159,7 @@ if [ ${#FAILED_CHECKS[@]} -eq 0 ]; then
|
||||
echo ""
|
||||
echo "Summary of checks:"
|
||||
echo " ✅ Code formatting (Prettier)"
|
||||
echo " ✅ Fast unit tests (Utils/Types/Services)"
|
||||
echo " ✅ Code linting (ESLint)"
|
||||
echo " ✅ Type checking (TypeScript)"
|
||||
echo " ⚠️ Markdown linting (non-critical)"
|
||||
|
||||
408
services/__tests__/mailgun.config.test.ts
Normal file
408
services/__tests__/mailgun.config.test.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import {
|
||||
getMailgunConfig,
|
||||
isMailgunConfigured,
|
||||
isDevelopmentMode,
|
||||
} from '../mailgun.config';
|
||||
|
||||
// Mock environment utilities
|
||||
jest.mock('../../utils/env', () => ({
|
||||
getEnvVar: jest.fn(),
|
||||
isProduction: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Mailgun Configuration', () => {
|
||||
let mockGetEnvVar: jest.MockedFunction<any>;
|
||||
let mockIsProduction: jest.MockedFunction<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Get mocked functions
|
||||
const envUtils = require('../../utils/env');
|
||||
mockGetEnvVar = envUtils.getEnvVar;
|
||||
mockIsProduction = envUtils.isProduction;
|
||||
|
||||
// Default mock implementations
|
||||
mockIsProduction.mockReturnValue(false);
|
||||
});
|
||||
|
||||
describe('getMailgunConfig', () => {
|
||||
test('should return config with all environment variables set', () => {
|
||||
mockGetEnvVar
|
||||
.mockReturnValueOnce('test-api-key') // VITE_MAILGUN_API_KEY
|
||||
.mockReturnValueOnce('test.domain.com') // VITE_MAILGUN_DOMAIN
|
||||
.mockReturnValueOnce('https://api.mailgun.net/v3') // VITE_MAILGUN_BASE_URL
|
||||
.mockReturnValueOnce('Test App') // VITE_MAILGUN_FROM_NAME
|
||||
.mockReturnValueOnce('noreply@test.com'); // VITE_MAILGUN_FROM_EMAIL
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(config).toEqual({
|
||||
apiKey: 'test-api-key',
|
||||
domain: 'test.domain.com',
|
||||
baseUrl: 'https://api.mailgun.net/v3',
|
||||
fromName: 'Test App',
|
||||
fromEmail: 'noreply@test.com',
|
||||
});
|
||||
|
||||
expect(mockGetEnvVar).toHaveBeenCalledTimes(5);
|
||||
expect(mockGetEnvVar).toHaveBeenNthCalledWith(1, 'VITE_MAILGUN_API_KEY');
|
||||
expect(mockGetEnvVar).toHaveBeenNthCalledWith(2, 'VITE_MAILGUN_DOMAIN');
|
||||
expect(mockGetEnvVar).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'VITE_MAILGUN_BASE_URL',
|
||||
'https://api.mailgun.net/v3'
|
||||
);
|
||||
expect(mockGetEnvVar).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'VITE_MAILGUN_FROM_NAME',
|
||||
'Medication Reminder'
|
||||
);
|
||||
expect(mockGetEnvVar).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'VITE_MAILGUN_FROM_EMAIL'
|
||||
);
|
||||
});
|
||||
|
||||
test('should use default values for optional config', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
if (key === 'VITE_MAILGUN_API_KEY') return 'test-api-key';
|
||||
if (key === 'VITE_MAILGUN_DOMAIN') return 'test.domain.com';
|
||||
if (key === 'VITE_MAILGUN_FROM_EMAIL') return 'noreply@test.com';
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(config.baseUrl).toBe('https://api.mailgun.net/v3');
|
||||
expect(config.fromName).toBe('Medication Reminder');
|
||||
});
|
||||
|
||||
test('should handle missing environment variables gracefully', () => {
|
||||
mockGetEnvVar.mockImplementation(
|
||||
(_key: string, defaultValue?: string) => defaultValue
|
||||
);
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(config).toEqual({
|
||||
apiKey: undefined,
|
||||
domain: undefined,
|
||||
baseUrl: 'https://api.mailgun.net/v3',
|
||||
fromName: 'Medication Reminder',
|
||||
fromEmail: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle empty string environment variables', () => {
|
||||
mockGetEnvVar.mockImplementation(
|
||||
(key: string, _defaultValue?: string) => {
|
||||
if (key === 'VITE_MAILGUN_API_KEY') return '';
|
||||
if (key === 'VITE_MAILGUN_DOMAIN') return '';
|
||||
if (key === 'VITE_MAILGUN_FROM_EMAIL') return '';
|
||||
if (key === 'VITE_MAILGUN_BASE_URL') return '';
|
||||
if (key === 'VITE_MAILGUN_FROM_NAME') return '';
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(config.apiKey).toBe('');
|
||||
expect(config.domain).toBe('');
|
||||
expect(config.baseUrl).toBe('');
|
||||
expect(config.fromName).toBe('');
|
||||
expect(config.fromEmail).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMailgunConfigured', () => {
|
||||
test('should return true when all required config is present', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'test-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'test.domain.com';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'noreply@test.com';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when apiKey is missing', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return undefined;
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'test.domain.com';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'noreply@test.com';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when domain is missing', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'test-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return undefined;
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'noreply@test.com';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when fromEmail is missing', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'test-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'test.domain.com';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return undefined;
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when apiKey is empty string', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return '';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'test.domain.com';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'noreply@test.com';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when domain is empty string', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'test-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return '';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'noreply@test.com';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when fromEmail is empty string', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'test-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'test.domain.com';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return '';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true even when optional fields are missing', () => {
|
||||
// Provide only required fields (optional ones fall back to defaults)
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'test-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'test.domain.com';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'noreply@test.com';
|
||||
// baseUrl and fromName return undefined so defaults are applied
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle whitespace strings correctly', () => {
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return ' test-key ';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return ' test.domain.com ';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return ' test@example.com ';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'Test App';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
expect(isMailgunConfigured()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDevelopmentMode', () => {
|
||||
test('should return true when not in production and Mailgun not configured', () => {
|
||||
mockIsProduction.mockReturnValue(false);
|
||||
|
||||
expect(isDevelopmentMode()).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when in production even if Mailgun not configured', () => {
|
||||
mockIsProduction.mockReturnValue(true);
|
||||
|
||||
expect(isDevelopmentMode()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when Mailgun is configured regardless of environment', () => {
|
||||
mockIsProduction.mockReturnValue(false);
|
||||
mockGetEnvVar
|
||||
.mockReturnValueOnce('test-api-key')
|
||||
.mockReturnValueOnce('test.domain.com')
|
||||
.mockReturnValueOnce('https://api.mailgun.net/v3')
|
||||
.mockReturnValueOnce('Test App')
|
||||
.mockReturnValueOnce('noreply@test.com');
|
||||
|
||||
expect(isDevelopmentMode()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when in production and Mailgun is configured', () => {
|
||||
mockIsProduction.mockReturnValue(true);
|
||||
mockGetEnvVar
|
||||
.mockReturnValueOnce('test-api-key')
|
||||
.mockReturnValueOnce('test.domain.com')
|
||||
.mockReturnValueOnce('https://api.mailgun.net/v3')
|
||||
.mockReturnValueOnce('Test App')
|
||||
.mockReturnValueOnce('noreply@test.com');
|
||||
|
||||
expect(isDevelopmentMode()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Removed validateMailgunConfig tests because validateMailgunConfig is not exported
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
test('should work with real environment configuration flow', () => {
|
||||
// Provide stable implementation for multiple calls
|
||||
mockGetEnvVar.mockImplementation((key: string, defaultValue?: string) => {
|
||||
switch (key) {
|
||||
case 'VITE_MAILGUN_API_KEY':
|
||||
return 'real-api-key';
|
||||
case 'VITE_MAILGUN_DOMAIN':
|
||||
return 'mg.example.com';
|
||||
case 'VITE_MAILGUN_BASE_URL':
|
||||
return 'https://api.mailgun.net/v3';
|
||||
case 'VITE_MAILGUN_FROM_NAME':
|
||||
return 'My App';
|
||||
case 'VITE_MAILGUN_FROM_EMAIL':
|
||||
return 'support@example.com';
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(isMailgunConfigured()).toBe(true);
|
||||
expect(isDevelopmentMode()).toBe(false);
|
||||
// validateMailgunConfig not tested because not exported
|
||||
expect(config).toEqual({
|
||||
apiKey: 'real-api-key',
|
||||
domain: 'mg.example.com',
|
||||
baseUrl: 'https://api.mailgun.net/v3',
|
||||
fromName: 'My App',
|
||||
fromEmail: 'support@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
test('should work with development environment flow', () => {
|
||||
// Simulate development environment with no config
|
||||
mockGetEnvVar.mockReturnValue(undefined);
|
||||
mockIsProduction.mockReturnValue(false);
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
expect(isDevelopmentMode()).toBe(true);
|
||||
// validateMailgunConfig not tested because not exported
|
||||
expect(config.apiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should work with production environment without config (error case)', () => {
|
||||
// Simulate production environment without proper config
|
||||
mockGetEnvVar.mockReturnValue(undefined);
|
||||
mockIsProduction.mockReturnValue(true);
|
||||
|
||||
const config = getMailgunConfig();
|
||||
|
||||
expect(isMailgunConfigured()).toBe(false);
|
||||
expect(isDevelopmentMode()).toBe(false);
|
||||
// validateMailgunConfig not tested because not exported
|
||||
expect(config.apiKey).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,9 @@
|
||||
// Mailgun Email Service
|
||||
// This service handles email sending via Mailgun API
|
||||
/**
|
||||
* Mailgun Email Service
|
||||
* This service handles email sending via Mailgun API
|
||||
*/
|
||||
|
||||
import {
|
||||
getMailgunConfig,
|
||||
isMailgunConfigured,
|
||||
isDevelopmentMode,
|
||||
type MailgunConfig,
|
||||
} from './mailgun.config';
|
||||
import { getMailgunConfig, type MailgunConfig } from './mailgun.config';
|
||||
|
||||
interface EmailTemplate {
|
||||
subject: string;
|
||||
@@ -97,19 +94,6 @@ export class MailgunService {
|
||||
|
||||
async sendEmail(to: string, template: EmailTemplate): Promise<boolean> {
|
||||
try {
|
||||
// In development mode or when Mailgun is not configured, just log the email
|
||||
if (isDevelopmentMode()) {
|
||||
console.warn('📧 Mock Email Sent (Development Mode):', {
|
||||
to,
|
||||
subject: template.subject,
|
||||
from: `${this.config.fromName} <${this.config.fromEmail}>`,
|
||||
html: template.html,
|
||||
text: template.text,
|
||||
note: 'To enable real emails, configure Mailgun credentials in environment variables',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Production Mailgun API call
|
||||
const formData = new FormData();
|
||||
formData.append(
|
||||
@@ -147,7 +131,7 @@ export class MailgunService {
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
console.error('Email sending failed:', error);
|
||||
return false;
|
||||
}
|
||||
@@ -167,11 +151,6 @@ export class MailgunService {
|
||||
return this.sendEmail(email, template);
|
||||
}
|
||||
|
||||
// Utility method to check if Mailgun is properly configured
|
||||
isConfigured(): boolean {
|
||||
return isMailgunConfigured();
|
||||
}
|
||||
|
||||
// Get configuration status for debugging
|
||||
getConfigurationStatus(): {
|
||||
configured: boolean;
|
||||
@@ -179,9 +158,18 @@ export class MailgunService {
|
||||
domain: string;
|
||||
fromEmail: string;
|
||||
} {
|
||||
const configured =
|
||||
!!this.config.apiKey &&
|
||||
!!this.config.domain &&
|
||||
!!this.config.baseUrl &&
|
||||
!!this.config.fromEmail &&
|
||||
!!this.config.fromName;
|
||||
const mode: 'development' | 'production' = configured
|
||||
? 'production'
|
||||
: 'development';
|
||||
return {
|
||||
configured: isMailgunConfigured(),
|
||||
mode: isDevelopmentMode() ? 'development' : 'production',
|
||||
configured,
|
||||
mode,
|
||||
domain: this.config.domain,
|
||||
fromEmail: this.config.fromEmail,
|
||||
};
|
||||
|
||||
308
types/__tests__/types.test.ts
Normal file
308
types/__tests__/types.test.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { Frequency, UserRole, DoseStatus } from '../../types';
|
||||
import type {
|
||||
Medication,
|
||||
Dose,
|
||||
CustomReminder,
|
||||
ReminderInstance,
|
||||
User,
|
||||
CouchDBDocument,
|
||||
} from '../../types';
|
||||
|
||||
describe('Type Definitions and Enums', () => {
|
||||
describe('Frequency Enum', () => {
|
||||
test('should have correct string values', () => {
|
||||
expect(Frequency.Daily).toBe('Daily');
|
||||
expect(Frequency.TwiceADay).toBe('Twice a day');
|
||||
expect(Frequency.ThreeTimesADay).toBe('Three times a day');
|
||||
expect(Frequency.EveryXHours).toBe('Every X hours');
|
||||
});
|
||||
|
||||
test('should be usable as object keys', () => {
|
||||
const frequencyMap = {
|
||||
[Frequency.Daily]: 1,
|
||||
[Frequency.TwiceADay]: 2,
|
||||
[Frequency.ThreeTimesADay]: 3,
|
||||
[Frequency.EveryXHours]: 'variable',
|
||||
};
|
||||
|
||||
expect(frequencyMap[Frequency.Daily]).toBe(1);
|
||||
expect(frequencyMap[Frequency.TwiceADay]).toBe(2);
|
||||
expect(frequencyMap[Frequency.ThreeTimesADay]).toBe(3);
|
||||
expect(frequencyMap[Frequency.EveryXHours]).toBe('variable');
|
||||
});
|
||||
|
||||
test('should have all expected frequency options', () => {
|
||||
const frequencyValues = Object.values(Frequency);
|
||||
expect(frequencyValues).toHaveLength(4);
|
||||
expect(frequencyValues).toContain('Daily');
|
||||
expect(frequencyValues).toContain('Twice a day');
|
||||
expect(frequencyValues).toContain('Three times a day');
|
||||
expect(frequencyValues).toContain('Every X hours');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserRole Enum', () => {
|
||||
test('should have correct values', () => {
|
||||
expect(UserRole.USER).toBe('USER');
|
||||
expect(UserRole.ADMIN).toBe('ADMIN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DoseStatus Enum', () => {
|
||||
test('should have correct values', () => {
|
||||
expect(DoseStatus.UPCOMING).toBe('UPCOMING');
|
||||
expect(DoseStatus.TAKEN).toBe('TAKEN');
|
||||
expect(DoseStatus.MISSED).toBe('MISSED');
|
||||
expect(DoseStatus.SNOOZED).toBe('SNOOZED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CouchDBDocument Type', () => {
|
||||
test('should require _id and _rev fields', () => {
|
||||
const doc: CouchDBDocument = {
|
||||
_id: 'test-id',
|
||||
_rev: 'test-rev',
|
||||
};
|
||||
|
||||
expect(doc._id).toBe('test-id');
|
||||
expect(doc._rev).toBe('test-rev');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Medication Type', () => {
|
||||
test('should accept valid medication object', () => {
|
||||
const medication: Medication = {
|
||||
_id: 'med-123',
|
||||
_rev: 'rev-456',
|
||||
name: 'Aspirin',
|
||||
dosage: '100mg',
|
||||
frequency: Frequency.Daily,
|
||||
startTime: '08:00',
|
||||
notes: 'Take with food',
|
||||
};
|
||||
|
||||
expect(medication._id).toBe('med-123');
|
||||
expect(medication.name).toBe('Aspirin');
|
||||
expect(medication.frequency).toBe(Frequency.Daily);
|
||||
expect(medication.startTime).toBe('08:00');
|
||||
});
|
||||
|
||||
test('should accept medication with hoursBetween for EveryXHours frequency', () => {
|
||||
const medication: Medication = {
|
||||
_id: 'med-456',
|
||||
_rev: 'rev-789',
|
||||
name: 'Pain Reliever',
|
||||
dosage: '200mg',
|
||||
frequency: Frequency.EveryXHours,
|
||||
startTime: '06:00',
|
||||
hoursBetween: 6,
|
||||
notes: '',
|
||||
};
|
||||
|
||||
expect(medication.frequency).toBe(Frequency.EveryXHours);
|
||||
expect(medication.hoursBetween).toBe(6);
|
||||
});
|
||||
|
||||
test('should accept medication with optional fields undefined', () => {
|
||||
const medication: Medication = {
|
||||
_id: 'med-789',
|
||||
_rev: 'rev-012',
|
||||
name: 'Vitamin D',
|
||||
dosage: '1000 IU',
|
||||
frequency: Frequency.Daily,
|
||||
startTime: '07:00',
|
||||
hoursBetween: undefined,
|
||||
notes: '',
|
||||
};
|
||||
|
||||
expect(medication.hoursBetween).toBeUndefined();
|
||||
expect(medication.notes).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dose Type', () => {
|
||||
test('should accept valid dose object', () => {
|
||||
const scheduledTime = new Date('2024-01-15T08:00:00.000Z');
|
||||
const dose: Dose = {
|
||||
id: 'dose-123',
|
||||
medicationId: 'med-456',
|
||||
scheduledTime,
|
||||
};
|
||||
|
||||
expect(dose.id).toBe('dose-123');
|
||||
expect(dose.medicationId).toBe('med-456');
|
||||
expect(dose.scheduledTime).toBe(scheduledTime);
|
||||
expect(dose.scheduledTime).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
test('should validate unique dose ID format', () => {
|
||||
const dose: Dose = {
|
||||
id: 'med-123-2024-01-15-080000',
|
||||
medicationId: 'med-123',
|
||||
scheduledTime: new Date('2024-01-15T08:00:00.000Z'),
|
||||
};
|
||||
|
||||
expect(dose.id).toContain('med-123');
|
||||
expect(dose.id).toContain('2024-01-15');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CustomReminder Type', () => {
|
||||
test('should accept valid custom reminder object', () => {
|
||||
const reminder: CustomReminder = {
|
||||
_id: 'reminder-123',
|
||||
_rev: 'rev-456',
|
||||
title: 'Check Blood Pressure',
|
||||
icon: '🩺',
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
frequencyMinutes: 60,
|
||||
};
|
||||
|
||||
expect(reminder._id).toBe('reminder-123');
|
||||
expect(reminder.title).toBe('Check Blood Pressure');
|
||||
expect(reminder.icon).toBe('🩺');
|
||||
expect(reminder.startTime).toBe('09:00');
|
||||
expect(reminder.endTime).toBe('17:00');
|
||||
expect(reminder.frequencyMinutes).toBe(60);
|
||||
});
|
||||
|
||||
test('should validate frequency minutes is a number', () => {
|
||||
const reminder: CustomReminder = {
|
||||
_id: 'reminder-789',
|
||||
_rev: 'rev-012',
|
||||
title: 'Hydrate',
|
||||
icon: '💧',
|
||||
startTime: '08:00',
|
||||
endTime: '20:00',
|
||||
frequencyMinutes: 30,
|
||||
};
|
||||
|
||||
expect(typeof reminder.frequencyMinutes).toBe('number');
|
||||
expect(reminder.frequencyMinutes).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReminderInstance Type', () => {
|
||||
test('should accept valid reminder instance object', () => {
|
||||
const scheduledTime = new Date('2024-01-15T10:00:00.000Z');
|
||||
const instance: ReminderInstance = {
|
||||
id: 'instance-123',
|
||||
reminderId: 'reminder-456',
|
||||
title: 'Take Vitamins',
|
||||
icon: '💊',
|
||||
scheduledTime,
|
||||
};
|
||||
|
||||
expect(instance.id).toBe('instance-123');
|
||||
expect(instance.reminderId).toBe('reminder-456');
|
||||
expect(instance.title).toBe('Take Vitamins');
|
||||
expect(instance.icon).toBe('💊');
|
||||
expect(instance.scheduledTime).toBe(scheduledTime);
|
||||
expect(instance.scheduledTime).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Type', () => {
|
||||
test('should accept valid user object with required fields', () => {
|
||||
const user: User = {
|
||||
_id: 'user-123',
|
||||
_rev: 'rev-456',
|
||||
username: 'testuser',
|
||||
};
|
||||
|
||||
expect(user._id).toBe('user-123');
|
||||
expect(user._rev).toBe('rev-456');
|
||||
expect(user.username).toBe('testuser');
|
||||
});
|
||||
|
||||
test('should accept user with all optional fields', () => {
|
||||
const user: User = {
|
||||
_id: 'user-456',
|
||||
_rev: 'rev-789',
|
||||
username: 'fulluser',
|
||||
email: 'full@example.com',
|
||||
avatar: 'base64-encoded-avatar',
|
||||
password: 'hashed-password',
|
||||
emailVerified: true,
|
||||
role: UserRole.USER,
|
||||
createdAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
lastLoginAt: new Date('2024-01-15T08:00:00.000Z'),
|
||||
};
|
||||
|
||||
expect(user.email).toBe('full@example.com');
|
||||
expect(user.avatar).toBe('base64-encoded-avatar');
|
||||
expect(user.password).toBe('hashed-password');
|
||||
expect(user.emailVerified).toBe(true);
|
||||
expect(user.role).toBe(UserRole.USER);
|
||||
expect(user.createdAt).toBeInstanceOf(Date);
|
||||
expect(user.lastLoginAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type Compatibility', () => {
|
||||
test('should handle Date objects consistently', () => {
|
||||
const now = new Date();
|
||||
const dose: Dose = {
|
||||
id: 'date-test',
|
||||
medicationId: 'med-test',
|
||||
scheduledTime: now,
|
||||
};
|
||||
|
||||
expect(dose.scheduledTime).toBe(now);
|
||||
expect(dose.scheduledTime).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
test('should handle unicode characters in string fields', () => {
|
||||
const reminder: CustomReminder = {
|
||||
_id: 'unicode-test',
|
||||
_rev: 'rev-unicode',
|
||||
title: 'Medicine with émojis 💊🏥',
|
||||
icon: '🎯',
|
||||
startTime: '08:00',
|
||||
endTime: '20:00',
|
||||
frequencyMinutes: 30,
|
||||
};
|
||||
|
||||
expect(reminder.title).toContain('émojis');
|
||||
expect(reminder.title).toContain('💊');
|
||||
expect(reminder.icon).toBe('🎯');
|
||||
});
|
||||
|
||||
test('should handle empty string values where appropriate', () => {
|
||||
const medication: Medication = {
|
||||
_id: 'empty-test',
|
||||
_rev: 'rev-empty',
|
||||
name: 'Test Med',
|
||||
dosage: '10mg',
|
||||
frequency: Frequency.Daily,
|
||||
startTime: '08:00',
|
||||
notes: '',
|
||||
};
|
||||
|
||||
expect(medication.notes).toBe('');
|
||||
expect(typeof medication.notes).toBe('string');
|
||||
});
|
||||
|
||||
test('should maintain type safety with functions', () => {
|
||||
const createDose = (medication: Medication): Dose => ({
|
||||
id: `${medication._id}-${Date.now()}`,
|
||||
medicationId: medication._id,
|
||||
scheduledTime: new Date(),
|
||||
});
|
||||
|
||||
const medication: Medication = {
|
||||
_id: 'function-test',
|
||||
_rev: 'rev-function',
|
||||
name: 'Function Med',
|
||||
dosage: '25mg',
|
||||
frequency: Frequency.TwiceADay,
|
||||
startTime: '12:00',
|
||||
notes: '',
|
||||
};
|
||||
|
||||
const dose = createDose(medication);
|
||||
expect(dose.medicationId).toBe(medication._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
198
utils/__tests__/env.test.ts
Normal file
198
utils/__tests__/env.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
223
utils/__tests__/schedule.test.ts
Normal file
223
utils/__tests__/schedule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user