From 6d099348f7c2ed32555ba169e48e5840a54915e2 Mon Sep 17 00:00:00 2001 From: Rohan Saini Date: Thu, 28 May 2026 23:51:39 +0530 Subject: [PATCH] feat(frappe-api): implement StorageAuthService for persistent credentials (#37) --- poc-frappe-api/src/AuthService.ts | 66 ++++++++++++++++ poc-frappe-api/tests/AuthService.test.ts | 98 ++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 poc-frappe-api/src/AuthService.ts create mode 100644 poc-frappe-api/tests/AuthService.test.ts diff --git a/poc-frappe-api/src/AuthService.ts b/poc-frappe-api/src/AuthService.ts new file mode 100644 index 0000000..93a536b --- /dev/null +++ b/poc-frappe-api/src/AuthService.ts @@ -0,0 +1,66 @@ +export interface IAuthService { + getCredentials(): Promise<{ apiKey: string; apiSecret: string } | null>; + saveCredentials(apiKey: string, apiSecret: string): Promise; + clearCredentials(): Promise; +} + +export class MockAuthService implements IAuthService { + private apiKey: string | null = null; + private apiSecret: string | null = null; + + async getCredentials() { + if (!this.apiKey || !this.apiSecret) return null; + return { apiKey: this.apiKey, apiSecret: this.apiSecret }; + } + + async saveCredentials(apiKey: string, apiSecret: string) { + this.apiKey = apiKey; + this.apiSecret = apiSecret; + } + + async clearCredentials() { + this.apiKey = null; + this.apiSecret = null; + } +} + +export interface IStorageProvider { + getItem(key: string): string | null | Promise; + setItem(key: string, value: string): void | Promise; + removeItem(key: string): void | Promise; +} + +export class StorageAuthService implements IAuthService { + private storage: IStorageProvider; + private readonly storageKey: string; + + constructor(storage: IStorageProvider, storageKey: string = 'frappe_credentials') { + this.storage = storage; + this.storageKey = storageKey; + } + + public async getCredentials(): Promise<{ apiKey: string; apiSecret: string } | null> { + try { + const data = await this.storage.getItem(this.storageKey); + if (!data) { + return null; + } + const parsed = JSON.parse(data); + if (parsed && typeof parsed.apiKey === 'string' && typeof parsed.apiSecret === 'string') { + return { apiKey: parsed.apiKey, apiSecret: parsed.apiSecret }; + } + return null; + } catch { + return null; + } + } + + public async saveCredentials(apiKey: string, apiSecret: string): Promise { + const data = JSON.stringify({ apiKey, apiSecret }); + await this.storage.setItem(this.storageKey, data); + } + + public async clearCredentials(): Promise { + await this.storage.removeItem(this.storageKey); + } +} diff --git a/poc-frappe-api/tests/AuthService.test.ts b/poc-frappe-api/tests/AuthService.test.ts new file mode 100644 index 0000000..31885a9 --- /dev/null +++ b/poc-frappe-api/tests/AuthService.test.ts @@ -0,0 +1,98 @@ +import { StorageAuthService, IStorageProvider } from '../src/AuthService'; + +class SyncMockStorage implements IStorageProvider { + private store: { [key: string]: string } = {}; + + getItem(key: string): string | null { + return this.store[key] || null; + } + + setItem(key: string, value: string): void { + this.store[key] = value; + } + + removeItem(key: string): void { + delete this.store[key]; + } +} + +class AsyncMockStorage implements IStorageProvider { + private store: { [key: string]: string } = {}; + + async getItem(key: string): Promise { + return this.store[key] || null; + } + + async setItem(key: string, value: string): Promise { + this.store[key] = value; + } + + async removeItem(key: string): Promise { + delete this.store[key]; + } +} + +describe('StorageAuthService', () => { + describe('with Synchronous Storage', () => { + let storage: SyncMockStorage; + let authService: StorageAuthService; + + beforeEach(() => { + storage = new SyncMockStorage(); + authService = new StorageAuthService(storage); + }); + + it('should return null when credentials do not exist', async () => { + const creds = await authService.getCredentials(); + expect(creds).toBeNull(); + }); + + it('should save and retrieve credentials successfully', async () => { + await authService.saveCredentials('key123', 'secret456'); + const creds = await authService.getCredentials(); + expect(creds).toEqual({ apiKey: 'key123', apiSecret: 'secret456' }); + }); + + it('should clear credentials successfully', async () => { + await authService.saveCredentials('key123', 'secret456'); + await authService.clearCredentials(); + const creds = await authService.getCredentials(); + expect(creds).toBeNull(); + }); + + it('should handle corrupted JSON data gracefully by returning null', async () => { + storage.setItem('frappe_credentials', '{invalid_json}'); + const creds = await authService.getCredentials(); + expect(creds).toBeNull(); + }); + + it('should handle missing fields in stored JSON gracefully by returning null', async () => { + storage.setItem('frappe_credentials', JSON.stringify({ apiKey: 'only_key' })); + const creds = await authService.getCredentials(); + expect(creds).toBeNull(); + }); + }); + + describe('with Asynchronous Storage', () => { + let storage: AsyncMockStorage; + let authService: StorageAuthService; + + beforeEach(() => { + storage = new AsyncMockStorage(); + authService = new StorageAuthService(storage); + }); + + it('should save and retrieve credentials successfully over async boundary', async () => { + await authService.saveCredentials('asyncKey', 'asyncSecret'); + const creds = await authService.getCredentials(); + expect(creds).toEqual({ apiKey: 'asyncKey', apiSecret: 'asyncSecret' }); + }); + + it('should clear credentials successfully over async boundary', async () => { + await authService.saveCredentials('asyncKey', 'asyncSecret'); + await authService.clearCredentials(); + const creds = await authService.getCredentials(); + expect(creds).toBeNull(); + }); + }); +});