import { beforeEach, describe, expect, test } from 'bun:test'; import type { OAuthTokens } from '../client/config.js'; import { FileStorage, MemoryStorage, OAuthStorage, createStorageAdapter, } from '../client/storage.js'; describe('MemoryStorage', () => { let storage: MemoryStorage; beforeEach(() => { storage = new MemoryStorage(); }); test('should store and retrieve values', async () => { await storage.set('test-key', 'test-value'); const value = await storage.get('test-key'); expect(value).toBe('test-value'); }); test('should return undefined for missing keys', async () => { const value = await storage.get('non-existent'); expect(value).toBeUndefined(); }); test('should delete values', async () => { await storage.set('test-key', 'test-value'); await storage.delete('test-key'); const value = await storage.get('test-key'); expect(value).toBeUndefined(); }); test('should clear all data', async () => { await storage.set('key1', 'value1'); await storage.set('key2', 'value2'); storage.clear(); const value1 = await storage.get('key1'); const value2 = await storage.get('key2'); expect(value1).toBeUndefined(); expect(value2).toBeUndefined(); }); test('should handle multiple values', async () => { await storage.set('key1', 'value1'); await storage.set('key2', 'value2'); await storage.set('key3', 'value3'); const value1 = await storage.get('key1'); const value2 = await storage.get('key2'); const value3 = await storage.get('key3'); expect(value1).toBe('value1'); expect(value2).toBe('value2'); expect(value3).toBe('value3'); }); }); describe('FileStorage', () => { let storage: FileStorage; const testPath = './test-oauth-data'; beforeEach(async () => { storage = new FileStorage(testPath); await storage.clear(); // Clean up before each test }); test('should store and retrieve values', async () => { await storage.set('test-key', 'test-value'); const value = await storage.get('test-key'); expect(value).toBe('test-value'); }); test('should return undefined for missing keys', async () => { const value = await storage.get('non-existent'); expect(value).toBeUndefined(); }); test('should delete values', async () => { await storage.set('test-key', 'test-value'); await storage.delete('test-key'); const value = await storage.get('test-key'); expect(value).toBeUndefined(); }); test('should sanitize keys for filenames', async () => { const unsafeKey = 'test/key:with@special#chars'; await storage.set(unsafeKey, 'test-value'); const value = await storage.get(unsafeKey); expect(value).toBe('test-value'); }); test('should handle multiple values', async () => { await storage.set('key1', 'value1'); await storage.set('key2', 'value2'); const value1 = await storage.get('key1'); const value2 = await storage.get('key2'); expect(value1).toBe('value1'); expect(value2).toBe('value2'); }); test('should clear all data', async () => { await storage.set('key1', 'value1'); await storage.set('key2', 'value2'); await storage.clear(); const value1 = await storage.get('key1'); const value2 = await storage.get('key2'); expect(value1).toBeUndefined(); expect(value2).toBeUndefined(); }); }); describe('createStorageAdapter', () => { test('should create MemoryStorage by default', () => { const storage = createStorageAdapter(); expect(storage).toBeInstanceOf(MemoryStorage); }); test('should create MemoryStorage when type is "memory"', () => { const storage = createStorageAdapter('memory'); expect(storage).toBeInstanceOf(MemoryStorage); }); test('should create FileStorage when type is "file"', () => { const storage = createStorageAdapter('file'); expect(storage).toBeInstanceOf(FileStorage); }); test('should use custom path for FileStorage', async () => { const customPath = './custom-oauth-path'; const storage = createStorageAdapter('file', { path: customPath }); expect(storage).toBeInstanceOf(FileStorage); // Clean up if (storage instanceof FileStorage) { await storage.clear(); } }); }); describe('OAuthStorage', () => { let storage: MemoryStorage; let oauthStorage: OAuthStorage; const sessionId = 'test-session-id'; beforeEach(() => { storage = new MemoryStorage(); oauthStorage = new OAuthStorage(storage, sessionId); }); describe('token management', () => { test('should save and retrieve tokens', async () => { const tokens: OAuthTokens = { access_token: 'test-access-token', token_type: 'Bearer', expires_in: 3600, refresh_token: 'test-refresh-token', }; await oauthStorage.saveTokens(tokens); const retrieved = await oauthStorage.getTokens(); expect(retrieved).toBeDefined(); expect(retrieved?.access_token).toBe(tokens.access_token); expect(retrieved?.token_type).toBe(tokens.token_type); expect(retrieved?.refresh_token).toBe(tokens.refresh_token); // expires_in should be close to original value (within 5 seconds due to time passing) expect(retrieved?.expires_in).toBeGreaterThanOrEqual(3595); expect(retrieved?.expires_in).toBeLessThanOrEqual(3600); }); test('should return undefined when no tokens exist', async () => { const tokens = await oauthStorage.getTokens(); expect(tokens).toBeUndefined(); }); test('should clear tokens', async () => { const tokens: OAuthTokens = { access_token: 'test-access-token', token_type: 'Bearer', }; await oauthStorage.saveTokens(tokens); await oauthStorage.clearTokens(); const retrieved = await oauthStorage.getTokens(); expect(retrieved).toBeUndefined(); }); test('should handle tokens without optional fields', async () => { const tokens: OAuthTokens = { access_token: 'test-access-token', token_type: 'Bearer', }; await oauthStorage.saveTokens(tokens); const retrieved = await oauthStorage.getTokens(); expect(retrieved).toEqual(tokens); }); }); describe('client info management', () => { test('should save and retrieve client info', async () => { const clientInfo = { client_id: 'test-client-id', client_secret: 'test-client-secret', redirect_uris: ['http://localhost:8080/callback'], }; await oauthStorage.saveClientInfo(clientInfo); const retrieved = await oauthStorage.getClientInfo(); expect(retrieved).toEqual(clientInfo); }); test('should return undefined when no client info exists', async () => { const clientInfo = await oauthStorage.getClientInfo(); expect(clientInfo).toBeUndefined(); }); test('should handle client info without secret', async () => { const clientInfo = { client_id: 'test-client-id', redirect_uris: ['http://localhost:8080/callback'], }; await oauthStorage.saveClientInfo(clientInfo); const retrieved = await oauthStorage.getClientInfo(); expect(retrieved).toEqual(clientInfo); }); }); describe('code verifier management', () => { test('should save and retrieve code verifier', async () => { const verifier = 'test-code-verifier-12345'; await oauthStorage.saveCodeVerifier(verifier); const retrieved = await oauthStorage.getCodeVerifier(); expect(retrieved).toBe(verifier); }); test('should return undefined when no code verifier exists', async () => { const verifier = await oauthStorage.getCodeVerifier(); expect(verifier).toBeUndefined(); }); test('should clear code verifier', async () => { const verifier = 'test-code-verifier-12345'; await oauthStorage.saveCodeVerifier(verifier); await oauthStorage.clearCodeVerifier(); const retrieved = await oauthStorage.getCodeVerifier(); expect(retrieved).toBeUndefined(); }); }); describe('session isolation', () => { test('should isolate tokens between sessions', async () => { const session1 = new OAuthStorage(storage, 'session-1'); const session2 = new OAuthStorage(storage, 'session-2'); const tokens1: OAuthTokens = { access_token: 'token-1', token_type: 'Bearer', }; const tokens2: OAuthTokens = { access_token: 'token-2', token_type: 'Bearer', }; await session1.saveTokens(tokens1); await session2.saveTokens(tokens2); const retrieved1 = await session1.getTokens(); const retrieved2 = await session2.getTokens(); expect(retrieved1).toEqual(tokens1); expect(retrieved2).toEqual(tokens2); }); test('should correctly calculate expires_in over time', async () => { const tokens: OAuthTokens = { access_token: 'test-access-token', token_type: 'Bearer', expires_in: 3600, // 1 hour refresh_token: 'test-refresh-token', }; // Save tokens await oauthStorage.saveTokens(tokens); // Retrieve immediately - should be close to 3600 const retrieved1 = await oauthStorage.getTokens(); expect(retrieved1?.expires_in).toBeGreaterThanOrEqual(3595); expect(retrieved1?.expires_in).toBeLessThanOrEqual(3600); // Wait 2 seconds await new Promise(resolve => setTimeout(resolve, 2000)); // Retrieve again - should be approximately 2 seconds less const retrieved2 = await oauthStorage.getTokens(); expect(retrieved2?.expires_in).toBeGreaterThanOrEqual(3593); expect(retrieved2?.expires_in).toBeLessThanOrEqual(3598); expect(retrieved2?.expires_in).toBeLessThan(retrieved1!.expires_in!); }); }); });