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"],
|
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/services/**/__tests__/**/*.test.ts",
|
"<rootDir>/services/**/__tests__/**/*.test.ts",
|
||||||
|
"<rootDir>/utils/**/__tests__/**/*.test.ts",
|
||||||
|
"<rootDir>/types/**/__tests__/**/*.test.ts",
|
||||||
"<rootDir>/tests/**/*.test.ts",
|
"<rootDir>/tests/**/*.test.ts",
|
||||||
"<rootDir>/tests/**/*.test.js"
|
"<rootDir>/tests/**/*.test.js"
|
||||||
],
|
],
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
"components/**/*.tsx",
|
"components/**/*.tsx",
|
||||||
"hooks/**/*.ts",
|
"hooks/**/*.ts",
|
||||||
"utils/**/*.ts",
|
"utils/**/*.ts",
|
||||||
|
"types/**/*.ts",
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/__tests__/**"
|
"!**/__tests__/**"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage",
|
"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:integration": "jest tests/integration/production.test.js",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ print_status "Starting parallel checks..."
|
|||||||
# 1. Prettier formatting (fast, runs on all relevant files)
|
# 1. Prettier formatting (fast, runs on all relevant files)
|
||||||
run_check "prettier" "bun run pre-commit"
|
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)
|
STAGED_JS_TS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|jsx|ts|tsx)$' || true)
|
||||||
if [ -n "$STAGED_JS_TS_FILES" ]; then
|
if [ -n "$STAGED_JS_TS_FILES" ]; then
|
||||||
# Convert newlines to spaces for proper argument passing
|
# Convert newlines to spaces for proper argument passing
|
||||||
@@ -77,7 +80,7 @@ else
|
|||||||
echo "No JS/TS files to lint" > "$TEMP_DIR/eslint.out"
|
echo "No JS/TS files to lint" > "$TEMP_DIR/eslint.out"
|
||||||
fi
|
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)
|
STAGED_TS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx)$' || true)
|
||||||
if [ -n "$STAGED_TS_FILES" ]; then
|
if [ -n "$STAGED_TS_FILES" ]; then
|
||||||
run_check "typecheck" "./scripts/type-check-staged.sh"
|
run_check "typecheck" "./scripts/type-check-staged.sh"
|
||||||
@@ -86,7 +89,7 @@ else
|
|||||||
echo "No TypeScript files to check" > "$TEMP_DIR/typecheck.out"
|
echo "No TypeScript files to check" > "$TEMP_DIR/typecheck.out"
|
||||||
fi
|
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)
|
STAGED_MD_FILES=$(echo "$STAGED_FILES" | grep -E '\.md$' || true)
|
||||||
if [ -n "$STAGED_MD_FILES" ]; then
|
if [ -n "$STAGED_MD_FILES" ]; then
|
||||||
# Convert newlines to spaces for proper argument passing
|
# Convert newlines to spaces for proper argument passing
|
||||||
@@ -97,7 +100,7 @@ else
|
|||||||
echo "No markdown files to lint" > "$TEMP_DIR/markdown.out"
|
echo "No markdown files to lint" > "$TEMP_DIR/markdown.out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Secret scanning on staged files (optional check)
|
# 6. Secret scanning on staged files (optional check)
|
||||||
if command -v secretlint > /dev/null; then
|
if command -v secretlint > /dev/null; then
|
||||||
# Convert newlines to spaces for proper argument passing
|
# Convert newlines to spaces for proper argument passing
|
||||||
SECRET_FILES=$(echo "$STAGED_FILES" | tr '\n' ' ')
|
SECRET_FILES=$(echo "$STAGED_FILES" | tr '\n' ' ')
|
||||||
@@ -111,7 +114,7 @@ fi
|
|||||||
print_status "Waiting for checks to complete..."
|
print_status "Waiting for checks to complete..."
|
||||||
|
|
||||||
FAILED_CHECKS=()
|
FAILED_CHECKS=()
|
||||||
ALL_CHECKS=("prettier" "eslint" "typecheck" "markdown" "secrets")
|
ALL_CHECKS=("prettier" "fast-tests" "eslint" "typecheck" "markdown" "secrets")
|
||||||
|
|
||||||
for check in "${ALL_CHECKS[@]}"; do
|
for check in "${ALL_CHECKS[@]}"; do
|
||||||
if [ -f "$TEMP_DIR/$check.pid" ]; then
|
if [ -f "$TEMP_DIR/$check.pid" ]; then
|
||||||
@@ -124,7 +127,7 @@ for check in "${ALL_CHECKS[@]}"; do
|
|||||||
print_success "$check passed"
|
print_success "$check passed"
|
||||||
else
|
else
|
||||||
# For some checks, failure is not critical
|
# 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)"
|
print_warning "$check had issues (non-critical)"
|
||||||
if [ -f "$TEMP_DIR/$check.out" ] && [ -s "$TEMP_DIR/$check.out" ]; then
|
if [ -f "$TEMP_DIR/$check.out" ] && [ -s "$TEMP_DIR/$check.out" ]; then
|
||||||
echo -e "${YELLOW}$check output:${NC}"
|
echo -e "${YELLOW}$check output:${NC}"
|
||||||
@@ -156,6 +159,7 @@ if [ ${#FAILED_CHECKS[@]} -eq 0 ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Summary of checks:"
|
echo "Summary of checks:"
|
||||||
echo " ✅ Code formatting (Prettier)"
|
echo " ✅ Code formatting (Prettier)"
|
||||||
|
echo " ✅ Fast unit tests (Utils/Types/Services)"
|
||||||
echo " ✅ Code linting (ESLint)"
|
echo " ✅ Code linting (ESLint)"
|
||||||
echo " ✅ Type checking (TypeScript)"
|
echo " ✅ Type checking (TypeScript)"
|
||||||
echo " ⚠️ Markdown linting (non-critical)"
|
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 {
|
import { getMailgunConfig, type MailgunConfig } from './mailgun.config';
|
||||||
getMailgunConfig,
|
|
||||||
isMailgunConfigured,
|
|
||||||
isDevelopmentMode,
|
|
||||||
type MailgunConfig,
|
|
||||||
} from './mailgun.config';
|
|
||||||
|
|
||||||
interface EmailTemplate {
|
interface EmailTemplate {
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -97,19 +94,6 @@ export class MailgunService {
|
|||||||
|
|
||||||
async sendEmail(to: string, template: EmailTemplate): Promise<boolean> {
|
async sendEmail(to: string, template: EmailTemplate): Promise<boolean> {
|
||||||
try {
|
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
|
// Production Mailgun API call
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append(
|
formData.append(
|
||||||
@@ -147,7 +131,7 @@ export class MailgunService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Email sending failed:', error);
|
console.error('Email sending failed:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -167,11 +151,6 @@ export class MailgunService {
|
|||||||
return this.sendEmail(email, template);
|
return this.sendEmail(email, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility method to check if Mailgun is properly configured
|
|
||||||
isConfigured(): boolean {
|
|
||||||
return isMailgunConfigured();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration status for debugging
|
// Get configuration status for debugging
|
||||||
getConfigurationStatus(): {
|
getConfigurationStatus(): {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
@@ -179,9 +158,18 @@ export class MailgunService {
|
|||||||
domain: string;
|
domain: string;
|
||||||
fromEmail: 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 {
|
return {
|
||||||
configured: isMailgunConfigured(),
|
configured,
|
||||||
mode: isDevelopmentMode() ? 'development' : 'production',
|
mode,
|
||||||
domain: this.config.domain,
|
domain: this.config.domain,
|
||||||
fromEmail: this.config.fromEmail,
|
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