Skip to content

Commit fabc212

Browse files
committed
feat(frappe-api): implement StorageAuthService for persistent credentials storage (#38)
1 parent a068b6a commit fabc212

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

poc-frappe-api/src/AuthService.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
export interface IAuthService {
2+
getCredentials(): Promise<{ apiKey: string; apiSecret: string } | null>;
3+
saveCredentials(apiKey: string, apiSecret: string): Promise<void>;
4+
clearCredentials(): Promise<void>;
5+
}
6+
7+
export class MockAuthService implements IAuthService {
8+
private apiKey: string | null = null;
9+
private apiSecret: string | null = null;
10+
11+
async getCredentials() {
12+
if (!this.apiKey || !this.apiSecret) return null;
13+
return { apiKey: this.apiKey, apiSecret: this.apiSecret };
14+
}
15+
16+
async saveCredentials(apiKey: string, apiSecret: string) {
17+
this.apiKey = apiKey;
18+
this.apiSecret = apiSecret;
19+
}
20+
21+
async clearCredentials() {
22+
this.apiKey = null;
23+
this.apiSecret = null;
24+
}
25+
}
26+
27+
export interface IStorageProvider {
28+
getItem(key: string): string | null | Promise<string | null>;
29+
setItem(key: string, value: string): void | Promise<void>;
30+
removeItem(key: string): void | Promise<void>;
31+
}
32+
33+
export class StorageAuthService implements IAuthService {
34+
private storage: IStorageProvider;
35+
private readonly storageKey: string;
36+
37+
constructor(storage: IStorageProvider, storageKey: string = 'frappe_credentials') {
38+
this.storage = storage;
39+
this.storageKey = storageKey;
40+
}
41+
42+
public async getCredentials(): Promise<{ apiKey: string; apiSecret: string } | null> {
43+
try {
44+
const data = await this.storage.getItem(this.storageKey);
45+
if (!data) {
46+
return null;
47+
}
48+
const parsed = JSON.parse(data);
49+
if (parsed && typeof parsed.apiKey === 'string' && typeof parsed.apiSecret === 'string') {
50+
return { apiKey: parsed.apiKey, apiSecret: parsed.apiSecret };
51+
}
52+
return null;
53+
} catch {
54+
return null;
55+
}
56+
}
57+
58+
public async saveCredentials(apiKey: string, apiSecret: string): Promise<void> {
59+
const data = JSON.stringify({ apiKey, apiSecret });
60+
await this.storage.setItem(this.storageKey, data);
61+
}
62+
63+
public async clearCredentials(): Promise<void> {
64+
await this.storage.removeItem(this.storageKey);
65+
}
66+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { StorageAuthService, IStorageProvider } from '../src/AuthService';
2+
3+
class SyncMockStorage implements IStorageProvider {
4+
private store: { [key: string]: string } = {};
5+
6+
getItem(key: string): string | null {
7+
return this.store[key] || null;
8+
}
9+
10+
setItem(key: string, value: string): void {
11+
this.store[key] = value;
12+
}
13+
14+
removeItem(key: string): void {
15+
delete this.store[key];
16+
}
17+
}
18+
19+
class AsyncMockStorage implements IStorageProvider {
20+
private store: { [key: string]: string } = {};
21+
22+
async getItem(key: string): Promise<string | null> {
23+
return this.store[key] || null;
24+
}
25+
26+
async setItem(key: string, value: string): Promise<void> {
27+
this.store[key] = value;
28+
}
29+
30+
async removeItem(key: string): Promise<void> {
31+
delete this.store[key];
32+
}
33+
}
34+
35+
describe('StorageAuthService', () => {
36+
describe('with Synchronous Storage', () => {
37+
let storage: SyncMockStorage;
38+
let authService: StorageAuthService;
39+
40+
beforeEach(() => {
41+
storage = new SyncMockStorage();
42+
authService = new StorageAuthService(storage);
43+
});
44+
45+
it('should return null when credentials do not exist', async () => {
46+
const creds = await authService.getCredentials();
47+
expect(creds).toBeNull();
48+
});
49+
50+
it('should save and retrieve credentials successfully', async () => {
51+
await authService.saveCredentials('key123', 'secret456');
52+
const creds = await authService.getCredentials();
53+
expect(creds).toEqual({ apiKey: 'key123', apiSecret: 'secret456' });
54+
});
55+
56+
it('should clear credentials successfully', async () => {
57+
await authService.saveCredentials('key123', 'secret456');
58+
await authService.clearCredentials();
59+
const creds = await authService.getCredentials();
60+
expect(creds).toBeNull();
61+
});
62+
63+
it('should handle corrupted JSON data gracefully by returning null', async () => {
64+
storage.setItem('frappe_credentials', '{invalid_json}');
65+
const creds = await authService.getCredentials();
66+
expect(creds).toBeNull();
67+
});
68+
69+
it('should handle missing fields in stored JSON gracefully by returning null', async () => {
70+
storage.setItem('frappe_credentials', JSON.stringify({ apiKey: 'only_key' }));
71+
const creds = await authService.getCredentials();
72+
expect(creds).toBeNull();
73+
});
74+
});
75+
76+
describe('with Asynchronous Storage', () => {
77+
let storage: AsyncMockStorage;
78+
let authService: StorageAuthService;
79+
80+
beforeEach(() => {
81+
storage = new AsyncMockStorage();
82+
authService = new StorageAuthService(storage);
83+
});
84+
85+
it('should save and retrieve credentials successfully over async boundary', async () => {
86+
await authService.saveCredentials('asyncKey', 'asyncSecret');
87+
const creds = await authService.getCredentials();
88+
expect(creds).toEqual({ apiKey: 'asyncKey', apiSecret: 'asyncSecret' });
89+
});
90+
91+
it('should clear credentials successfully over async boundary', async () => {
92+
await authService.saveCredentials('asyncKey', 'asyncSecret');
93+
await authService.clearCredentials();
94+
const creds = await authService.getCredentials();
95+
expect(creds).toBeNull();
96+
});
97+
});
98+
});

0 commit comments

Comments
 (0)