feat: add DateUtils and StringUtils for date and string manipulation

- Implement DateUtils for handling date operations with timezone support.
- Add unit tests for DateUtils to ensure functionality and edge cases.
- Create StringUtils for string manipulation with a fluent API.
- Include unit tests for StringUtils covering various string operations.
- Introduce EncryptionUtils for secure data encryption and decryption.
- Add tests for EncryptionUtils to validate encryption and decryption processes.
- Update index.ts to export new utility modules.
- Introduce encryption key management with a dedicated key file.
This commit is contained in:
Firman Ramdhani
2026-01-28 18:18:29 +07:00
parent 97f48ce1b9
commit a1db9b93e1
15 changed files with 269 additions and 268 deletions
@@ -0,0 +1 @@
export const ENC_STORAGE_KEY = 'zkwqyo3RpNEh8un2CIAs'; //TODO change value from environment
@@ -0,0 +1,125 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EncryptionUtils } from './encryption.utils';
import { AES } from 'crypto-js';
// 1. Mock the key module to ensure test consistency
vi.mock('./encryption-key', () => ({
ENC_STORAGE_KEY: 'default-test-key-123',
}));
describe('EncryptionUtils', () => {
const TEST_KEY = 'secret-key-xyz';
let service: EncryptionUtils;
beforeEach(() => {
// Reset the Singleton instance to ensure clean state for every test
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(EncryptionUtils as any).instance = undefined;
service = new EncryptionUtils(TEST_KEY);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Constructor', () => {
it('should create an instance with the provided key', () => {
const instance = new EncryptionUtils('custom-key');
expect(instance).toBeInstanceOf(EncryptionUtils);
});
it('should use the default ENC_STORAGE_KEY if no key is provided', () => {
const instance = new EncryptionUtils();
// Verify functionality with default key
const data = 'test-default';
const encrypted = instance.encrypt(data);
expect(instance.decrypt(encrypted)).toBe(data);
});
it('should throw an Error if the provided key is empty', () => {
expect(() => new EncryptionUtils('')).toThrow('[EncryptionUtils] Encryption key is missing/empty.');
});
});
describe('encrypt()', () => {
it('should return an encrypted string different from the plain text', () => {
const plainText = 'Hello World';
const encrypted = service.encrypt(plainText);
expect(encrypted).not.toBe(plainText);
expect(encrypted).not.toBe('');
expect(typeof encrypted).toBe('string');
});
it('should return an empty string if input is empty', () => {
expect(service.encrypt('')).toBe('');
});
});
describe('decrypt()', () => {
it('should decrypt data back to the original text correctly', () => {
const plainText = 'Secret Data 123';
const encrypted = service.encrypt(plainText);
const decrypted = service.decrypt(encrypted);
expect(decrypted).toBe(plainText);
});
it('should return an empty string if input is empty', () => {
expect(service.decrypt('')).toBe('');
});
it('should return an empty string if decrypted with the WRONG key', () => {
const plainText = 'Sensitive Data';
// Encrypt with Service A (TEST_KEY)
const encrypted = service.encrypt(plainText);
// Decrypt with Service B (WRONG_KEY)
const wrongService = new EncryptionUtils('wrong-key-999');
const result = wrongService.decrypt(encrypted);
expect(result).toBe('');
});
it('should return an empty string if input is malformed (Graceful Fail)', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const malformedData = 'invalid-base64-string-@@@';
const result = service.decrypt(malformedData);
expect(result).toBe('');
// NOTE: crypto-js handles garbage input gracefully without throwing,
// so console.error is NOT called here.
});
it('should catch error and log it when decryption throws an exception (Critical Fail)', () => {
/**
* Spy on console.error to suppress and track error logging during the test.
* Mocked to prevent error messages from appearing in test output.
*/
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// FORCE an error by mocking AES.decrypt implementation
vi.spyOn(AES, 'decrypt').mockImplementationOnce(() => {
throw new Error('Simulated Crypto Failure');
});
const result = service.decrypt('any-data');
expect(result).toBe('');
// Now we expect the catch block to be executed
expect(consoleSpy).toHaveBeenCalledWith('[EncryptionUtils] Decryption failed:', expect.any(Error));
});
});
describe('getInstance() (Singleton)', () => {
it('should return the same instance reference', () => {
const instance1 = EncryptionUtils.getInstance();
const instance2 = EncryptionUtils.getInstance();
expect(instance1).toBe(instance2);
});
});
});
@@ -0,0 +1,76 @@
import { AES, enc } from 'crypto-js';
import { ENC_STORAGE_KEY } from './encryption-key';
/**
* Interface defining encryption service methods.
*/
interface IEncryptionUtils {
encrypt(data: string): string;
decrypt(encryptedData: string): string;
}
export class EncryptionUtils implements IEncryptionUtils {
private readonly _key: string;
/**
* @param key - Encryption key used for encrypting and decrypting data.
* If not provided, defaults to ENC_STORAGE_KEY.
*/
constructor(key: string = ENC_STORAGE_KEY) {
if (!key) {
throw new Error('[EncryptionUtils] Encryption key is missing/empty.');
}
this._key = key;
}
/**
* Encrypts the given data string.
* Returns an empty string '' if input data is empty.
*/
public encrypt(data: string): string {
if (!data) {
return '';
}
return AES.encrypt(data, this._key).toString();
}
/**
* Decrypts the given encrypted data string.
* Returns an empty string '' if input is empty or decryption fails.
*/
public decrypt(encryptedData: string): string {
if (!encryptedData) {
return '';
}
try {
const bytes = AES.decrypt(encryptedData, this._key);
const originalText = bytes.toString(enc.Utf8);
// If decryption yields an empty string, consider it a failure
if (!originalText) {
return '';
}
return originalText;
} catch (error) {
console.error('[EncryptionUtils] Decryption failed:', error);
return '';
}
}
/**
* Get the singleton instance of EncryptionUtils.
* Uses the default ENC_STORAGE_KEY.
* @return EncryptionUtils instance
*/
private static instance: EncryptionUtils;
public static getInstance(): EncryptionUtils {
if (!EncryptionUtils.instance) {
EncryptionUtils.instance = new EncryptionUtils();
}
return EncryptionUtils.instance;
}
}