feat: implement DateService with timezone support and add EncryptionService with unit tests
This commit is contained in:
Vendored
+2
-1
@@ -3,5 +3,6 @@
|
|||||||
{
|
{
|
||||||
"mode": "auto"
|
"mode": "auto"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"cSpell.words": ["Ujung", "Pandang", "WITA"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { DateServiceComponent } from '@repo/ui';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
export default function AppModule() {
|
export default function AppModule() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/example/*" element={<div>1</div>} />
|
<Route path="/example/*" element={<DateServiceComponent />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ export const DateServiceComponent: React.FC = () => {
|
|||||||
<span className="opacity-60 text-xs ml-1">({currentTime.format('Z')})</span>
|
<span className="opacity-60 text-xs ml-1">({currentTime.format('Z')})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm font-bold text-blue-700 bg-blue-100 px-2 py-1 rounded border border-blue-200">
|
||||||
|
{currentTime.timezoneWithOffset}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-lg text-gray-500 mt-2">{currentTime.format('dddd, DD MMMM YYYY')}</div>
|
<div className="text-lg text-gray-500 mt-2">{currentTime.format('dddd, DD MMMM YYYY')}</div>
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import { DateService } from './date-service';
|
||||||
|
|
||||||
|
describe('DateService', () => {
|
||||||
|
// Setup standard environment before each test
|
||||||
|
beforeEach(() => {
|
||||||
|
// 1. Reset Global Timezone to UTC for deterministic results across environments
|
||||||
|
DateService.setGlobalConfig('UTC');
|
||||||
|
|
||||||
|
// 2. Mock console warn/error to keep the terminal clean during error handling tests
|
||||||
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up all mocks and restore real timers after each test
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should create an instance with the current time (now) using static method', () => {
|
||||||
|
const date = DateService.now();
|
||||||
|
expect(date).toBeInstanceOf(DateService);
|
||||||
|
expect(date.toDate()).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an instance from a specific ISO string', () => {
|
||||||
|
// Use full ISO format (.000Z) for precision
|
||||||
|
const input = '2025-01-01T10:00:00.000Z';
|
||||||
|
const date = new DateService(input);
|
||||||
|
expect(date.toISOString()).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a clone from another DateService instance', () => {
|
||||||
|
const original = new DateService('2025-01-01T00:00:00.000Z');
|
||||||
|
const clone = new DateService(original);
|
||||||
|
|
||||||
|
expect(clone.toISOString()).toBe(original.toISOString());
|
||||||
|
expect(clone).not.toBe(original); // Ensure references are different (Memory Address check)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid input by falling back to current time (Graceful Fallback)', () => {
|
||||||
|
// Freeze system time for precise assertions
|
||||||
|
const fixedTime = new Date('2025-01-01T12:00:00.000Z');
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(fixedTime);
|
||||||
|
|
||||||
|
const invalidDate = new DateService('invalid-date-string-xyz');
|
||||||
|
|
||||||
|
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Invalid date input'));
|
||||||
|
expect(invalidDate.toISOString()).toBe(fixedTime.toISOString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Global Configuration', () => {
|
||||||
|
it('should update global timezone correctly', () => {
|
||||||
|
DateService.setGlobalConfig('Asia/Jakarta');
|
||||||
|
expect(DateService.getGlobalTimezone()).toBe('Asia/Jakarta');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid timezone gracefully', () => {
|
||||||
|
const initialTz = DateService.getGlobalTimezone();
|
||||||
|
DateService.setGlobalConfig('Mars/Alien_City');
|
||||||
|
|
||||||
|
expect(console.error).toHaveBeenCalled();
|
||||||
|
expect(DateService.getGlobalTimezone()).toBe(initialTz);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply timezone to new instances', () => {
|
||||||
|
const utcString = '2025-01-01T00:00:00Z';
|
||||||
|
|
||||||
|
// Test UTC
|
||||||
|
DateService.setGlobalConfig('UTC');
|
||||||
|
const dateUtc = new DateService(utcString);
|
||||||
|
expect(dateUtc.format('HH:mm')).toBe('00:00');
|
||||||
|
|
||||||
|
// Test Jakarta (UTC+7)
|
||||||
|
DateService.setGlobalConfig('Asia/Jakarta');
|
||||||
|
const dateJkt = new DateService(utcString);
|
||||||
|
expect(dateJkt.format('HH:mm')).toBe('07:00');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Immutability & Manipulation', () => {
|
||||||
|
// This verifies that .clone() is working effectively
|
||||||
|
it('should be immutable on "add"', () => {
|
||||||
|
const start = new DateService('2025-01-01T00:00:00.000Z');
|
||||||
|
const nextDay = start.add(1, 'day');
|
||||||
|
|
||||||
|
expect(start.format('DD')).toBe('01'); // Original instance MUST NOT change
|
||||||
|
expect(nextDay.format('DD')).toBe('02'); // New instance should reflect the change
|
||||||
|
expect(start).not.toBe(nextDay); // Ensure they are different objects
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be immutable on "subtract"', () => {
|
||||||
|
const start = new DateService('2025-01-02T00:00:00.000Z');
|
||||||
|
const prevDay = start.subtract(1, 'day');
|
||||||
|
|
||||||
|
expect(start.format('DD')).toBe('02'); // Original instance MUST NOT change
|
||||||
|
expect(prevDay.format('DD')).toBe('01');
|
||||||
|
expect(start).not.toBe(prevDay);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be immutable on "startOf"', () => {
|
||||||
|
const date = new DateService('2025-01-15T12:00:00.000Z');
|
||||||
|
const startOfMonth = date.startOf('month');
|
||||||
|
|
||||||
|
expect(date.format('DD')).toBe('15'); // Original remains 15th
|
||||||
|
expect(startOfMonth.format('DD')).toBe('01'); // New instance is 1st
|
||||||
|
expect(startOfMonth).not.toBe(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be immutable on "endOf"', () => {
|
||||||
|
const date = new DateService('2025-01-01T00:00:00.000Z');
|
||||||
|
const endOfDay = date.endOf('day');
|
||||||
|
|
||||||
|
// Format HH:mm:ss depends on global timezone (currently UTC)
|
||||||
|
expect(endOfDay.format('HH:mm:ss')).toBe('23:59:59');
|
||||||
|
expect(endOfDay).not.toBe(date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Formatting & Comparison', () => {
|
||||||
|
it('should format date string correctly', () => {
|
||||||
|
const date = new DateService('2025-12-25T00:00:00.000Z');
|
||||||
|
expect(date.format('DD/MM/YYYY')).toBe('25/12/2025');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return strict ISO 8601 string (always UTC)', () => {
|
||||||
|
DateService.setGlobalConfig('Asia/Jakarta');
|
||||||
|
// Input Jakarta local time 7 AM = 0 AM UTC
|
||||||
|
const date = new DateService('2025-01-01T07:00:00+07:00');
|
||||||
|
expect(date.toISOString()).toBe('2025-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compare dates correctly', () => {
|
||||||
|
const d1 = new DateService('2025-01-01T00:00:00.000Z');
|
||||||
|
const d2 = new DateService('2025-01-02T00:00:00.000Z');
|
||||||
|
|
||||||
|
expect(d1.isBefore(d2)).toBe(true);
|
||||||
|
expect(d2.isAfter(d1)).toBe(true);
|
||||||
|
expect(d1.isSame(d1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate precise diff', () => {
|
||||||
|
const d1 = new DateService('2025-01-01T00:00:00.000Z');
|
||||||
|
const d2 = new DateService('2025-01-03T00:00:00.000Z');
|
||||||
|
expect(d2.diff(d1, 'day')).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate calendar day diff (ignoring time)', () => {
|
||||||
|
// Case: 23:00 vs 01:00 the next day
|
||||||
|
// Technically only 2 hours difference (0 full days), but 1 calendar day difference.
|
||||||
|
const d1 = new DateService('2025-01-01T23:00:00.000Z');
|
||||||
|
const d2 = new DateService('2025-01-02T01:00:00.000Z');
|
||||||
|
|
||||||
|
expect(d2.diff(d1, 'day')).toBe(0); // Standard diff (less than 24h)
|
||||||
|
expect(d2.diffCalendarDay(d1)).toBe(1); // Calendar diff
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Timezone Utilities (Indonesian & Offset)', () => {
|
||||||
|
it('should return custom mapping for Indonesia (WIB)', () => {
|
||||||
|
DateService.setGlobalConfig('Asia/Jakarta');
|
||||||
|
const date = new DateService();
|
||||||
|
expect(date.timezoneAbbr).toBe('WIB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return custom mapping for Indonesia (WITA)', () => {
|
||||||
|
DateService.setGlobalConfig('Asia/Makassar');
|
||||||
|
const date = new DateService();
|
||||||
|
expect(date.timezoneAbbr).toBe('WITA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return custom mapping for Indonesia (WIT)', () => {
|
||||||
|
DateService.setGlobalConfig('Asia/Jayapura');
|
||||||
|
const date = new DateService();
|
||||||
|
expect(date.timezoneAbbr).toBe('WIT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to standard abbr for other zones', () => {
|
||||||
|
DateService.setGlobalConfig('UTC');
|
||||||
|
const date = new DateService();
|
||||||
|
expect(date.timezoneAbbr).toBe('UTC');
|
||||||
|
});
|
||||||
|
|
||||||
|
// NEW FEATURE TEST: timezoneWithOffset
|
||||||
|
it('should return combined timezone abbreviation and offset', () => {
|
||||||
|
const isoInput = '2025-01-01T12:00:00Z'; // Input in UTC
|
||||||
|
|
||||||
|
// Case 1: Jakarta
|
||||||
|
DateService.setGlobalConfig('Asia/Jakarta');
|
||||||
|
const dateJkt = new DateService(isoInput);
|
||||||
|
// Jakarta = UTC+7
|
||||||
|
expect(dateJkt.timezoneWithOffset).toBe('WIB (+07:00)');
|
||||||
|
|
||||||
|
// Case 2: Makassar
|
||||||
|
DateService.setGlobalConfig('Asia/Makassar');
|
||||||
|
const dateMks = new DateService(isoInput);
|
||||||
|
// Makassar = UTC+8
|
||||||
|
expect(dateMks.timezoneWithOffset).toBe('WITA (+08:00)');
|
||||||
|
|
||||||
|
// Case 3: UTC
|
||||||
|
DateService.setGlobalConfig('UTC');
|
||||||
|
const dateUtc = new DateService(isoInput);
|
||||||
|
expect(dateUtc.timezoneWithOffset).toBe('UTC (+00:00)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Epoch & Helpers', () => {
|
||||||
|
it('should return correct epoch values', () => {
|
||||||
|
const input = 1704067200000; // 2024-01-01 00:00:00 UTC
|
||||||
|
const date = new DateService(input);
|
||||||
|
|
||||||
|
expect(date.timestamp).toBe(input);
|
||||||
|
expect(date.epochMillis).toBe(input);
|
||||||
|
expect(date.epochSeconds).toBe(1704067200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return supported timezones list', () => {
|
||||||
|
const timezones = DateService.getSupportedTimezones();
|
||||||
|
expect(Array.isArray(timezones)).toBe(true);
|
||||||
|
expect(timezones).toContain('Asia/Jakarta');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+22
-4
@@ -192,19 +192,19 @@ export class DateService implements IDateManager {
|
|||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
|
|
||||||
add(value: number, unit: TimeUnit): DateService {
|
add(value: number, unit: TimeUnit): DateService {
|
||||||
return new DateService(this._date.add(value, unit as ManipulateType));
|
return new DateService(this._date.clone().add(value, unit as ManipulateType));
|
||||||
}
|
}
|
||||||
|
|
||||||
subtract(value: number, unit: TimeUnit): DateService {
|
subtract(value: number, unit: TimeUnit): DateService {
|
||||||
return new DateService(this._date.subtract(value, unit as ManipulateType));
|
return new DateService(this._date.clone().subtract(value, unit as ManipulateType));
|
||||||
}
|
}
|
||||||
|
|
||||||
startOf(unit: TimeUnit): DateService {
|
startOf(unit: TimeUnit): DateService {
|
||||||
return new DateService(this._date.startOf(unit as OpUnitType));
|
return new DateService(this._date.clone().startOf(unit as OpUnitType));
|
||||||
}
|
}
|
||||||
|
|
||||||
endOf(unit: TimeUnit): DateService {
|
endOf(unit: TimeUnit): DateService {
|
||||||
return new DateService(this._date.endOf(unit as OpUnitType));
|
return new DateService(this._date.clone().endOf(unit as OpUnitType));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -293,4 +293,22 @@ export class DateService implements IDateManager {
|
|||||||
const tz = DateService._defaultTimezone;
|
const tz = DateService._defaultTimezone;
|
||||||
return INDONESIA_TZ_MAP[tz] ?? this._date.format('z');
|
return INDONESIA_TZ_MAP[tz] ?? this._date.format('z');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* Timezone Utilities
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// ... (method getSupportedTimezones dan get timezoneAbbr yang sudah ada)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns timezone abbreviation combined with GMT offset.
|
||||||
|
* Useful for UI labels.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "WIB (+07:00)"
|
||||||
|
* "UTC (+00:00)"
|
||||||
|
*/
|
||||||
|
get timezoneWithOffset(): string {
|
||||||
|
return `${this.timezoneAbbr} (${this._date.format('Z')})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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';
|
import { ENC_STORAGE_KEY } from './encryption-key';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface contract supaya method konsisten
|
* Interface defining encryption service methods.
|
||||||
*/
|
*/
|
||||||
interface IEncryptionService {
|
interface IEncryptionService {
|
||||||
encrypt(data: string): string;
|
encrypt(data: string): string;
|
||||||
@@ -13,7 +13,8 @@ export class EncryptionService implements IEncryptionService {
|
|||||||
private readonly _key: string;
|
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) {
|
constructor(key: string = ENC_STORAGE_KEY) {
|
||||||
if (!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 {
|
public encrypt(data: string): string {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -33,8 +35,8 @@ export class EncryptionService implements IEncryptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mendekripsi string terenkripsi.
|
* Decrypts the given encrypted data string.
|
||||||
* Mengembalikan string kosong '' jika gagal decrypt atau format salah.
|
* Returns an empty string '' if input is empty or decryption fails.
|
||||||
*/
|
*/
|
||||||
public decrypt(encryptedData: string): string {
|
public decrypt(encryptedData: string): string {
|
||||||
if (!encryptedData) {
|
if (!encryptedData) {
|
||||||
@@ -45,7 +47,7 @@ export class EncryptionService implements IEncryptionService {
|
|||||||
const bytes = AES.decrypt(encryptedData, this._key);
|
const bytes = AES.decrypt(encryptedData, this._key);
|
||||||
const originalText = bytes.toString(enc.Utf8);
|
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) {
|
if (!originalText) {
|
||||||
return '';
|
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;
|
private static instance: EncryptionService;
|
||||||
|
|
||||||
public static getInstance(): EncryptionService {
|
public static getInstance(): EncryptionService {
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function utilsExample() {
|
|
||||||
return 'This is an example utility function.';
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './encryption';
|
export * from './encryption/encryption-key';
|
||||||
export * from './example';
|
export * from './encryption/encryption-service';
|
||||||
export * from './date-service';
|
|
||||||
|
export * from './date-service/date-service';
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const add = (a: number, b: number) => a + b;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const subtract = (a: number, b: number) => a - b;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { expect, test } from 'vitest';
|
|
||||||
import { add } from './add';
|
|
||||||
import { subtract } from './subtract';
|
|
||||||
|
|
||||||
test('adds 1 + 2 to equal 3', () => {
|
|
||||||
expect(add(1, 2)).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('subtracts 2 - 1 to equal 1', () => {
|
|
||||||
expect(subtract(2, 1)).toBe(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user