feat: implement DateService with timezone support and add EncryptionService with unit tests
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { EncryptionService } from './encryption-service';
|
||||
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('EncryptionService', () => {
|
||||
const TEST_KEY = 'secret-key-xyz';
|
||||
let service: EncryptionService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the Singleton instance to ensure clean state for every test
|
||||
(EncryptionService as any).instance = undefined;
|
||||
service = new EncryptionService(TEST_KEY);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Constructor', () => {
|
||||
it('should create an instance with the provided key', () => {
|
||||
const instance = new EncryptionService('custom-key');
|
||||
expect(instance).toBeInstanceOf(EncryptionService);
|
||||
});
|
||||
|
||||
it('should use the default ENC_STORAGE_KEY if no key is provided', () => {
|
||||
const instance = new EncryptionService();
|
||||
// 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 EncryptionService('')).toThrow('[EncryptionService] 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 EncryptionService('wrong-key-999');
|
||||
const result = wrongService.decrypt(encrypted);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return an empty string if input is malformed (Graceful Fail)', () => {
|
||||
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('[EncryptionService] Decryption failed:', expect.any(Error));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInstance() (Singleton)', () => {
|
||||
it('should return the same instance reference', () => {
|
||||
const instance1 = EncryptionService.getInstance();
|
||||
const instance2 = EncryptionService.getInstance();
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
});
|
||||
});
|
||||
+14
-8
@@ -2,7 +2,7 @@ import { AES, enc } from 'crypto-js';
|
||||
import { ENC_STORAGE_KEY } from './encryption-key';
|
||||
|
||||
/**
|
||||
* Interface contract supaya method konsisten
|
||||
* Interface defining encryption service methods.
|
||||
*/
|
||||
interface IEncryptionService {
|
||||
encrypt(data: string): string;
|
||||
@@ -13,7 +13,8 @@ export class EncryptionService implements IEncryptionService {
|
||||
private readonly _key: string;
|
||||
|
||||
/**
|
||||
* @param key (Optional) Jika tidak diisi, otomatis pakai ENC_STORAGE_KEY
|
||||
* @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) {
|
||||
@@ -23,7 +24,8 @@ export class EncryptionService implements IEncryptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengenkripsi string plain text.
|
||||
* Encrypts the given data string.
|
||||
* Returns an empty string '' if input data is empty.
|
||||
*/
|
||||
public encrypt(data: string): string {
|
||||
if (!data) {
|
||||
@@ -33,8 +35,8 @@ export class EncryptionService implements IEncryptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendekripsi string terenkripsi.
|
||||
* Mengembalikan string kosong '' jika gagal decrypt atau format salah.
|
||||
* Decrypts the given encrypted data string.
|
||||
* Returns an empty string '' if input is empty or decryption fails.
|
||||
*/
|
||||
public decrypt(encryptedData: string): string {
|
||||
if (!encryptedData) {
|
||||
@@ -45,7 +47,7 @@ export class EncryptionService implements IEncryptionService {
|
||||
const bytes = AES.decrypt(encryptedData, this._key);
|
||||
const originalText = bytes.toString(enc.Utf8);
|
||||
|
||||
// Validasi tambahan: jika hasil decrypt kosong, berarti key salah atau data corrupt
|
||||
// If decryption yields an empty string, consider it a failure
|
||||
if (!originalText) {
|
||||
return '';
|
||||
}
|
||||
@@ -57,8 +59,12 @@ export class EncryptionService implements IEncryptionService {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Static Helper (Singleton Pattern sederhana) ---
|
||||
// Supaya tidak perlu 'new EncryptionService()' berulang kali
|
||||
/**
|
||||
* Get the singleton instance of EncryptionService.
|
||||
* Uses the default ENC_STORAGE_KEY.
|
||||
* @return EncryptionService instance
|
||||
*/
|
||||
|
||||
private static instance: EncryptionService;
|
||||
|
||||
public static getInstance(): EncryptionService {
|
||||
Reference in New Issue
Block a user