Merge origin/master
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { cache } from './cache';
|
||||
|
||||
describe('Cache', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
cache.clear(); // Clear cache before each test
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
cache.clear(); // Clean up after each test
|
||||
});
|
||||
|
||||
describe('set and get', () => {
|
||||
it('should set and retrieve a value', () => {
|
||||
cache.set('key', 'value');
|
||||
expect(cache.get('key')).toBe('value');
|
||||
});
|
||||
|
||||
it('should set multiple values with different keys', () => {
|
||||
cache.set('key1', 'value1');
|
||||
cache.set('key2', 'value2');
|
||||
cache.set('key3', 'value3');
|
||||
|
||||
expect(cache.get('key1')).toBe('value1');
|
||||
expect(cache.get('key2')).toBe('value2');
|
||||
expect(cache.get('key3')).toBe('value3');
|
||||
});
|
||||
|
||||
it('should overwrite existing value for same key', () => {
|
||||
cache.set('key', 'value1');
|
||||
expect(cache.get('key')).toBe('value1');
|
||||
|
||||
cache.set('key', 'value2');
|
||||
expect(cache.get('key')).toBe('value2');
|
||||
});
|
||||
|
||||
it('should return null for non-existing key', () => {
|
||||
expect(cache.get('nonexistent')).toBeNull();
|
||||
});
|
||||
|
||||
it('should store different types of values', () => {
|
||||
cache.set('string', 'text');
|
||||
cache.set('number', 42);
|
||||
cache.set('boolean', true);
|
||||
cache.set('object', { name: 'test' });
|
||||
cache.set('array', [1, 2, 3]);
|
||||
cache.set('null', null);
|
||||
|
||||
expect(cache.get('string')).toBe('text');
|
||||
expect(cache.get('number')).toBe(42);
|
||||
expect(cache.get('boolean')).toBe(true);
|
||||
expect(cache.get('object')).toEqual({ name: 'test' });
|
||||
expect(cache.get('array')).toEqual([1, 2, 3]);
|
||||
expect(cache.get('null')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TTL expiration', () => {
|
||||
it('should use default TTL of 5 minutes', () => {
|
||||
cache.set('test', 'value');
|
||||
|
||||
// Should be available before TTL expires
|
||||
expect(cache.get('test')).toBe('value');
|
||||
|
||||
// Advance time by 4 minutes 59 seconds
|
||||
vi.advanceTimersByTime(4 * 60 * 1000 + 59 * 1000);
|
||||
expect(cache.get('test')).toBe('value');
|
||||
|
||||
// Advance time by 2 more seconds (total 5 minutes 1 second)
|
||||
vi.advanceTimersByTime(2 * 1000);
|
||||
expect(cache.get('test')).toBeNull();
|
||||
});
|
||||
|
||||
it('should accept custom TTL per item', () => {
|
||||
cache.set('short', 'value', 1000); // 1 second TTL
|
||||
cache.set('long', 'value', 10000); // 10 seconds TTL
|
||||
|
||||
// Both should be available initially
|
||||
expect(cache.get('short')).toBe('value');
|
||||
expect(cache.get('long')).toBe('value');
|
||||
|
||||
// After 1.5 seconds, short should expire but long should remain
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(cache.get('short')).toBeNull();
|
||||
expect(cache.get('long')).toBe('value');
|
||||
|
||||
// After 9 more seconds (total 10.5), long should expire
|
||||
vi.advanceTimersByTime(9000);
|
||||
expect(cache.get('long')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for expired item', () => {
|
||||
cache.set('key', 'value', 1000);
|
||||
|
||||
// Should be available initially
|
||||
expect(cache.get('key')).toBe('value');
|
||||
|
||||
// Should be null after expiration
|
||||
vi.advanceTimersByTime(1001);
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete expired item from storage', () => {
|
||||
cache.set('key', 'value', 1000);
|
||||
|
||||
// Item should exist
|
||||
expect(cache.get('key')).toBe('value');
|
||||
|
||||
// After expiration, get should delete it
|
||||
vi.advanceTimersByTime(1001);
|
||||
expect(cache.get('key')).toBeNull();
|
||||
|
||||
// Subsequent get should also return null (item was deleted)
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return value at exact TTL boundary', () => {
|
||||
cache.set('key', 'value', 1000);
|
||||
|
||||
// At exactly TTL time, should still be valid
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(cache.get('key')).toBe('value');
|
||||
|
||||
// One millisecond later, should expire
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should respect different TTLs for different items', () => {
|
||||
cache.set('item1', 'value1', 1000);
|
||||
cache.set('item2', 'value2', 2000);
|
||||
cache.set('item3', 'value3', 3000);
|
||||
|
||||
// All items should be available initially
|
||||
expect(cache.get('item1')).toBe('value1');
|
||||
expect(cache.get('item2')).toBe('value2');
|
||||
expect(cache.get('item3')).toBe('value3');
|
||||
|
||||
// After 1.5 seconds, item1 should expire
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(cache.get('item1')).toBeNull();
|
||||
expect(cache.get('item2')).toBe('value2');
|
||||
expect(cache.get('item3')).toBe('value3');
|
||||
|
||||
// After 1 more second (total 2.5), item2 should expire
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(cache.get('item1')).toBeNull();
|
||||
expect(cache.get('item2')).toBeNull();
|
||||
expect(cache.get('item3')).toBe('value3');
|
||||
|
||||
// After 1 more second (total 3.5), item3 should expire
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(cache.get('item1')).toBeNull();
|
||||
expect(cache.get('item2')).toBeNull();
|
||||
expect(cache.get('item3')).toBeNull();
|
||||
});
|
||||
|
||||
it('should reset TTL when item is overwritten', () => {
|
||||
cache.set('key', 'value1', 1000);
|
||||
|
||||
// Advance time by 500ms
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
// Overwrite with new value and new TTL
|
||||
cache.set('key', 'value2', 2000);
|
||||
|
||||
// After 1500ms more (total 2000ms from first set), should still be available
|
||||
// because TTL was reset
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(cache.get('key')).toBe('value2');
|
||||
|
||||
// After 600ms more (2100ms from second set), should expire
|
||||
vi.advanceTimersByTime(600);
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle zero TTL', () => {
|
||||
cache.set('key', 'value', 0);
|
||||
|
||||
// Should expire immediately
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle very long TTL', () => {
|
||||
const oneDayInMs = 24 * 60 * 60 * 1000;
|
||||
cache.set('key', 'value', oneDayInMs);
|
||||
|
||||
// Should be available after 23 hours
|
||||
vi.advanceTimersByTime(23 * 60 * 60 * 1000);
|
||||
expect(cache.get('key')).toBe('value');
|
||||
|
||||
// Should still be available at exactly 24 hours
|
||||
vi.advanceTimersByTime(60 * 60 * 1000);
|
||||
expect(cache.get('key')).toBe('value');
|
||||
|
||||
// Should expire after 24 hours + 1ms
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('should return true for existing key', () => {
|
||||
cache.set('key', 'value');
|
||||
expect(cache.has('key')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existing key', () => {
|
||||
expect(cache.has('nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for expired key', () => {
|
||||
cache.set('key', 'value', 1000);
|
||||
|
||||
expect(cache.has('key')).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(1001);
|
||||
expect(cache.has('key')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null values', () => {
|
||||
cache.set('key', null);
|
||||
expect(cache.has('key')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete existing key', () => {
|
||||
cache.set('key', 'value');
|
||||
expect(cache.get('key')).toBe('value');
|
||||
|
||||
cache.delete('key');
|
||||
expect(cache.get('key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle deleting non-existing key', () => {
|
||||
expect(() => cache.delete('nonexistent')).not.toThrow();
|
||||
expect(cache.get('nonexistent')).toBeNull();
|
||||
});
|
||||
|
||||
it('should only delete specified key', () => {
|
||||
cache.set('key1', 'value1');
|
||||
cache.set('key2', 'value2');
|
||||
cache.set('key3', 'value3');
|
||||
|
||||
cache.delete('key2');
|
||||
|
||||
expect(cache.get('key1')).toBe('value1');
|
||||
expect(cache.get('key2')).toBeNull();
|
||||
expect(cache.get('key3')).toBe('value3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear all items from cache', () => {
|
||||
cache.set('key1', 'value1');
|
||||
cache.set('key2', 'value2');
|
||||
cache.set('key3', 'value3');
|
||||
|
||||
cache.clear();
|
||||
|
||||
expect(cache.get('key1')).toBeNull();
|
||||
expect(cache.get('key2')).toBeNull();
|
||||
expect(cache.get('key3')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle clearing empty cache', () => {
|
||||
expect(() => cache.clear()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow setting items after clear', () => {
|
||||
cache.set('key', 'value1');
|
||||
cache.clear();
|
||||
cache.set('key', 'value2');
|
||||
|
||||
expect(cache.get('key')).toBe('value2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@ export const CURRENT_ROLE = {
|
||||
|
||||
export const PROFILE = {
|
||||
name: "Damjan Savić",
|
||||
title: "AI & Automation Specialist",
|
||||
subtitle: "Building AI agents and automation solutions",
|
||||
title: "Fullstack Developer",
|
||||
subtitle: "Building websites, apps and SaaS with Next.js, React and TypeScript",
|
||||
languages: ["German", "English", "Serbian", "French", "Spanish", "Russian"]
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AppError, errorCodes, handleError, createAPIError } from './errorHandling';
|
||||
import * as analytics from './analytics';
|
||||
|
||||
// Mock the analytics module
|
||||
vi.mock('./analytics', () => ({
|
||||
trackEvent: vi.fn()
|
||||
}));
|
||||
|
||||
describe('errorHandling', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('AppError', () => {
|
||||
it('should create an AppError with all properties', () => {
|
||||
const error = new AppError('Test error', 'TEST_CODE', 'error', { key: 'value' });
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).toBeInstanceOf(AppError);
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.code).toBe('TEST_CODE');
|
||||
expect(error.severity).toBe('error');
|
||||
expect(error.metadata).toEqual({ key: 'value' });
|
||||
expect(error.name).toBe('AppError');
|
||||
});
|
||||
|
||||
it('should create an AppError with default severity', () => {
|
||||
const error = new AppError('Test error', 'TEST_CODE');
|
||||
|
||||
expect(error.severity).toBe('error');
|
||||
expect(error.metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create an AppError with warning severity', () => {
|
||||
const error = new AppError('Test warning', 'TEST_CODE', 'warning');
|
||||
|
||||
expect(error.severity).toBe('warning');
|
||||
});
|
||||
|
||||
it('should create an AppError with info severity', () => {
|
||||
const error = new AppError('Test info', 'TEST_CODE', 'info');
|
||||
|
||||
expect(error.severity).toBe('info');
|
||||
});
|
||||
});
|
||||
|
||||
describe('errorCodes', () => {
|
||||
it('should have all error codes defined', () => {
|
||||
expect(errorCodes.NETWORK_ERROR).toBe('ERR_NETWORK');
|
||||
expect(errorCodes.API_ERROR).toBe('ERR_API');
|
||||
expect(errorCodes.AUTH_ERROR).toBe('ERR_AUTH');
|
||||
expect(errorCodes.VALIDATION_ERROR).toBe('ERR_VALIDATION');
|
||||
expect(errorCodes.UNKNOWN_ERROR).toBe('ERR_UNKNOWN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleError', () => {
|
||||
it('should handle AppError and return it unchanged', () => {
|
||||
const originalError = new AppError('Test error', 'TEST_CODE', 'error', { foo: 'bar' });
|
||||
const result = handleError(originalError, 'test-context');
|
||||
|
||||
expect(result).toBe(originalError);
|
||||
expect(result.message).toBe('Test error');
|
||||
expect(result.code).toBe('TEST_CODE');
|
||||
expect(result.severity).toBe('error');
|
||||
expect(result.metadata).toEqual({ foo: 'bar' });
|
||||
expect(analytics.trackEvent).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'TEST_CODE',
|
||||
'test-context: Test error'
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert regular Error to AppError', () => {
|
||||
const originalError = new Error('Regular error');
|
||||
const result = handleError(originalError, 'test-context');
|
||||
|
||||
expect(result).toBeInstanceOf(AppError);
|
||||
expect(result.message).toBe('Regular error');
|
||||
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
|
||||
expect(result.severity).toBe('error');
|
||||
expect(result.metadata).toEqual({ originalError });
|
||||
expect(analytics.trackEvent).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
errorCodes.UNKNOWN_ERROR,
|
||||
'test-context: Regular error'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle unknown error types', () => {
|
||||
const result = handleError('string error', 'test-context');
|
||||
|
||||
expect(result).toBeInstanceOf(AppError);
|
||||
expect(result.message).toBe('An unexpected error occurred');
|
||||
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
|
||||
expect(result.severity).toBe('error');
|
||||
expect(result.metadata).toEqual({ originalError: 'string error' });
|
||||
expect(analytics.trackEvent).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
errorCodes.UNKNOWN_ERROR,
|
||||
'test-context: An unexpected error occurred'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null error', () => {
|
||||
const result = handleError(null, 'test-context');
|
||||
|
||||
expect(result).toBeInstanceOf(AppError);
|
||||
expect(result.message).toBe('An unexpected error occurred');
|
||||
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
|
||||
expect(result.metadata).toEqual({ originalError: null });
|
||||
});
|
||||
|
||||
it('should handle undefined error', () => {
|
||||
const result = handleError(undefined, 'test-context');
|
||||
|
||||
expect(result).toBeInstanceOf(AppError);
|
||||
expect(result.message).toBe('An unexpected error occurred');
|
||||
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
|
||||
expect(result.metadata).toEqual({ originalError: undefined });
|
||||
});
|
||||
|
||||
it('should track error in analytics', () => {
|
||||
const error = new AppError('Analytics test', 'ANALYTICS_CODE');
|
||||
handleError(error, 'analytics-context');
|
||||
|
||||
expect(analytics.trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(analytics.trackEvent).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'ANALYTICS_CODE',
|
||||
'analytics-context: Analytics test'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAPIError', () => {
|
||||
it('should create an API error with status code', () => {
|
||||
const error = createAPIError(404, 'Not found');
|
||||
|
||||
expect(error).toBeInstanceOf(AppError);
|
||||
expect(error.message).toBe('Not found');
|
||||
expect(error.code).toBe(errorCodes.API_ERROR);
|
||||
expect(error.severity).toBe('error');
|
||||
expect(error.metadata).toEqual({ status: 404 });
|
||||
});
|
||||
|
||||
it('should create an API error for 500 status', () => {
|
||||
const error = createAPIError(500, 'Internal server error');
|
||||
|
||||
expect(error).toBeInstanceOf(AppError);
|
||||
expect(error.message).toBe('Internal server error');
|
||||
expect(error.code).toBe(errorCodes.API_ERROR);
|
||||
expect(error.metadata).toEqual({ status: 500 });
|
||||
});
|
||||
|
||||
it('should create an API error for 401 status', () => {
|
||||
const error = createAPIError(401, 'Unauthorized');
|
||||
|
||||
expect(error).toBeInstanceOf(AppError);
|
||||
expect(error.message).toBe('Unauthorized');
|
||||
expect(error.metadata).toEqual({ status: 401 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user