Skip to main content
Glama
larksuite

Feishu/Lark OpenAPI MCP

Official
by larksuite
store.test.ts18.8 kB
import { AuthStore, authStore } from '../../src/auth/store'; import { generatePKCEPair } from '../../src/auth/utils/pkce'; import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; import { storageManager } from '../../src/auth/utils/storage-manager'; // Mock fs to prevent file system access jest.mock('fs', () => ({ existsSync: jest.fn(), watch: jest.fn(), readFileSync: jest.fn(), writeFileSync: jest.fn(), mkdirSync: jest.fn(), })); // Mock StorageManager jest.mock('../../src/auth/utils/storage-manager', () => ({ storageManager: { loadStorageData: jest.fn(), saveStorageData: jest.fn(), encrypt: jest.fn(), decrypt: jest.fn(), storageFile: '/mock/storage/path/storage.json', }, })); // Mock isTokenExpired jest.mock('../../src/auth/utils/is-token-valid', () => ({ isTokenExpired: jest.fn(), })); import { isTokenExpired } from '../../src/auth/utils/is-token-valid'; import fs from 'fs'; const mockStorageManager = storageManager as jest.Mocked<typeof storageManager>; const mockIsTokenExpired = isTokenExpired as jest.MockedFunction<typeof isTokenExpired>; const mockFs = fs as jest.Mocked<typeof fs>; describe('AuthStore', () => { let testAuthStore: AuthStore; beforeEach(() => { jest.clearAllMocks(); // Setup default mock implementations mockStorageManager.loadStorageData.mockResolvedValue({ tokens: {}, clients: {} }); mockStorageManager.saveStorageData.mockResolvedValue(); mockStorageManager.encrypt.mockImplementation((data) => `encrypted:${data}`); mockStorageManager.decrypt.mockImplementation((data) => data.replace('encrypted:', '')); // Mock fs methods to prevent file system errors mockFs.existsSync.mockReturnValue(false); mockFs.watch.mockReturnValue({} as any); testAuthStore = new AuthStore(); }); afterEach(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }); describe('Expired Token Cleanup', () => { it('should handle expired token cleanup and local token management', async () => { jest.clearAllMocks(); const now = Date.now() / 1000; const expiredToken: AuthInfo = { token: 'expired-token', clientId: 'test-client', scopes: ['read'], expiresAt: now - 8 * 24 * 60 * 60, // Expired 8 days ago }; const validToken: AuthInfo = { token: 'valid-token', clientId: 'test-client', scopes: ['read'], expiresAt: now + 3600, // Valid for 1 hour }; const tokenWithoutExpiry: AuthInfo = { token: 'no-expiry-token', clientId: 'test-client', scopes: ['read'], }; mockStorageManager.loadStorageData.mockResolvedValue({ tokens: { 'expired-token': expiredToken, 'valid-token': validToken, 'no-expiry-token': tokenWithoutExpiry, }, clients: {}, localTokens: { app1: 'expired-token', app2: 'valid-token', }, }); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 200)); const testToken: AuthInfo = { token: 'test-token', clientId: 'test-client', scopes: ['read'], expiresAt: now + 3600, }; await newAuthStore.storeToken(testToken); // Verify cleanup worked expect(mockStorageManager.saveStorageData).toHaveBeenCalled(); const retrievedExpiredToken = await newAuthStore.getToken('expired-token'); const retrievedValidToken = await newAuthStore.getToken('valid-token'); const retrievedNoExpiryToken = await newAuthStore.getToken('no-expiry-token'); expect(retrievedExpiredToken).toBeUndefined(); expect(retrievedValidToken).toEqual(validToken); expect(retrievedNoExpiryToken).toEqual(tokenWithoutExpiry); // Should not be removed }); it('should skip cleanup when no tokens are expired', async () => { jest.clearAllMocks(); const now = Date.now() / 1000; const validToken: AuthInfo = { token: 'valid-token', clientId: 'test-client', scopes: ['read'], expiresAt: now + 3600, }; mockStorageManager.loadStorageData.mockResolvedValue({ tokens: { 'valid-token': validToken }, clients: {}, }); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 50)); expect(mockStorageManager.saveStorageData).not.toHaveBeenCalled(); }); }); describe('Token Management', () => { it('should handle complete token lifecycle', async () => { const token: AuthInfo = { token: 'test-access-token', clientId: 'test-client', scopes: ['read', 'write'], expiresAt: Date.now() / 1000 + 3600, }; // Store token const storedToken = await testAuthStore.storeToken(token); expect(storedToken).toEqual(token); expect(mockStorageManager.saveStorageData).toHaveBeenCalled(); // Retrieve token const retrievedToken = await testAuthStore.getToken(token.token); expect(retrievedToken).toEqual(token); // Remove token await testAuthStore.removeToken(token.token); expect(mockStorageManager.saveStorageData).toHaveBeenCalledTimes(2); const removedToken = await testAuthStore.getToken(token.token); expect(removedToken).toBeUndefined(); }); it('should handle refresh token operations', async () => { const refreshToken = 'test-refresh-token'; const token: AuthInfo = { token: 'test-access-token', clientId: 'test-client', scopes: ['read'], extra: { refreshToken: refreshToken }, }; await testAuthStore.storeToken(token); // Retrieve by refresh token const retrievedToken = await testAuthStore.getTokenByRefreshToken(refreshToken); expect(retrievedToken).toEqual(token); // Test non-existent refresh token const nonExistentToken = await testAuthStore.getTokenByRefreshToken('non-existent'); expect(nonExistentToken).toBeUndefined(); }); it('should return undefined for non-existent operations', async () => { expect(await testAuthStore.getToken('non-existent')).toBeUndefined(); expect(await testAuthStore.getTokenByRefreshToken('non-existent')).toBeUndefined(); }); }); describe('Local Access Token Management', () => { it('should handle local token lifecycle with app management', async () => { const accessToken1 = 'token-app1'; const accessToken2 = 'token-app2'; const appId1 = 'app1'; const appId2 = 'app2'; // Store tokens for different apps await testAuthStore.storeLocalAccessToken(accessToken1, appId1); await testAuthStore.storeLocalAccessToken(accessToken2, appId2); // Verify retrieval expect(await testAuthStore.getLocalAccessToken(appId1)).toBe(accessToken1); expect(await testAuthStore.getLocalAccessToken(appId2)).toBe(accessToken2); // Test get all tokens const allTokens = await testAuthStore.getAllLocalAccessTokens(); expect(allTokens).toEqual({ [appId1]: accessToken1, [appId2]: accessToken2, }); // Remove one app's token await testAuthStore.removeLocalAccessToken(appId1); expect(await testAuthStore.getLocalAccessToken(appId1)).toBeUndefined(); expect(await testAuthStore.getLocalAccessToken(appId2)).toBe(accessToken2); // Remove all tokens await testAuthStore.removeAllLocalAccessTokens(); const finalTokens = await testAuthStore.getAllLocalAccessTokens(); expect(finalTokens).toEqual({}); }); it('should handle local token removal with associated token cleanup', async () => { const accessToken = 'local-access-token'; const appId = 'test-app-id'; await testAuthStore.storeLocalAccessToken(accessToken, appId); // Store corresponding token in tokens cache const token: AuthInfo = { token: accessToken, clientId: 'test-client', scopes: ['read'], }; await testAuthStore.storeToken(token); // Remove local access token should also remove associated token await testAuthStore.removeLocalAccessToken(appId); expect(await testAuthStore.getLocalAccessToken(appId)).toBeUndefined(); expect(await testAuthStore.getToken(accessToken)).toBeUndefined(); }); it('should handle edge cases gracefully', async () => { // Non-existent app expect(await testAuthStore.getLocalAccessToken('non-existent')).toBeUndefined(); // Remove non-existent token await testAuthStore.removeLocalAccessToken('non-existent'); // Empty object when localTokens is undefined mockStorageManager.loadStorageData.mockResolvedValue({ tokens: {}, clients: {}, }); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 10)); const allTokens = await newAuthStore.getAllLocalAccessTokens(); expect(allTokens).toEqual({}); }); }); describe('Client Management', () => { it('should handle client registration and operations', async () => { const client = { client_id: 'test-client-id', client_secret: 'test-client-secret', redirect_uris: ['http://localhost:3000/callback'], }; // Register client const registeredClient = await testAuthStore.registerClient(client); expect(registeredClient).toEqual(client); expect(mockStorageManager.saveStorageData).toHaveBeenCalled(); // Retrieve client const retrievedClient = await testAuthStore.getClient(client.client_id); expect(retrievedClient).toEqual(client); // Remove client await testAuthStore.removeClient(client.client_id); expect(mockStorageManager.saveStorageData).toHaveBeenCalledTimes(2); const removedClient = await testAuthStore.getClient(client.client_id); expect(removedClient).toBeUndefined(); }); it('should return undefined for non-existent client', async () => { const retrievedClient = await testAuthStore.getClient('non-existent-client'); expect(retrievedClient).toBeUndefined(); }); }); describe('Code Verifier Management', () => { it('should handle code verifier operations', async () => { const { codeVerifier: verifier1 } = generatePKCEPair(); const { codeVerifier: verifier2 } = generatePKCEPair(); const key1 = 'key1'; const key2 = 'key2'; // Store verifiers testAuthStore.storeCodeVerifier(key1, verifier1); testAuthStore.storeCodeVerifier(key2, verifier2); // Retrieve verifiers expect(testAuthStore.getCodeVerifier(key1)).toBe(verifier1); expect(testAuthStore.getCodeVerifier(key2)).toBe(verifier2); // Overwrite existing verifier const { codeVerifier: newVerifier } = generatePKCEPair(); testAuthStore.storeCodeVerifier(key1, newVerifier); expect(testAuthStore.getCodeVerifier(key1)).toBe(newVerifier); // Remove verifier testAuthStore.removeCodeVerifier(key1); expect(testAuthStore.getCodeVerifier(key1)).toBeUndefined(); // Clear all verifiers testAuthStore.clearExpiredCodeVerifiers(); expect(testAuthStore.getCodeVerifier(key2)).toBeUndefined(); }); it('should return undefined for non-existent code verifier', async () => { const retrievedVerifier = testAuthStore.getCodeVerifier('non-existent'); expect(retrievedVerifier).toBeUndefined(); }); }); describe('Constructor and Initialization', () => { it('should load data from storage on construction', async () => { const validToken: AuthInfo = { token: 'valid-token', clientId: 'test-client', scopes: ['read'], expiresAt: Date.now() / 1000 + 3600, }; mockStorageManager.loadStorageData.mockResolvedValue({ tokens: { 'valid-token': validToken }, clients: {}, localTokens: { app1: 'valid-token' }, // Use the same token that exists }); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 100)); expect(mockStorageManager.loadStorageData).toHaveBeenCalled(); const retrievedToken = await newAuthStore.getToken('valid-token'); expect(retrievedToken).toEqual(validToken); const retrievedLocalToken = await newAuthStore.getLocalAccessToken('app1'); expect(retrievedLocalToken).toBe('valid-token'); }); it('should handle initialization errors gracefully', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); mockStorageManager.loadStorageData.mockRejectedValue(new Error('Load failed')); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 100)); expect(consoleSpy).toHaveBeenCalledWith('[AuthStore] Failed to initialize: Error: Load failed'); consoleSpy.mockRestore(); }); it('should reuse initialization promise for concurrent calls', async () => { mockStorageManager.loadStorageData.mockClear(); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 10)); const promises = [ newAuthStore.getToken('test'), newAuthStore.getLocalAccessToken('test-app').catch(() => undefined), newAuthStore.getToken('test2'), ]; await Promise.all(promises); expect(mockStorageManager.loadStorageData).toHaveBeenCalledTimes(1); }); }); describe('Error Handling', () => { it('should handle storage errors in core operations', async () => { mockStorageManager.saveStorageData.mockRejectedValue(new Error('Storage save failed')); const token: AuthInfo = { token: 'test-token', clientId: 'test-client', scopes: ['read'], }; const client = { client_id: 'test-client-id', client_secret: 'test-client-secret', redirect_uris: ['http://localhost:3000/callback'], }; // All operations should fail with the same error await expect(testAuthStore.storeToken(token)).rejects.toThrow('Storage save failed'); await expect(testAuthStore.removeToken('test-token')).rejects.toThrow('Storage save failed'); await expect(testAuthStore.storeLocalAccessToken('test-token', 'test-app')).rejects.toThrow( 'Storage save failed', ); await expect(testAuthStore.removeLocalAccessToken('test-app')).rejects.toThrow('Storage save failed'); await expect(testAuthStore.registerClient(client)).rejects.toThrow('Storage save failed'); await expect(testAuthStore.removeClient('test-client')).rejects.toThrow('Storage save failed'); }); it('should skip saving when initialization failed', async () => { mockStorageManager.loadStorageData.mockRejectedValue(new Error('Init error')); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 100)); const testToken: AuthInfo = { token: 'test-token', clientId: 'test-client', scopes: ['read'], }; // Should not throw, but also should not save to storage await newAuthStore.storeToken(testToken); expect(mockStorageManager.saveStorageData).not.toHaveBeenCalled(); }); it('should handle missing data in clearExpiredTokens', async () => { const newAuthStore = new AuthStore(); // Set invalid cache data (newAuthStore as any).storageDataCache = null; await (newAuthStore as any).clearExpiredTokens(); expect(mockStorageManager.saveStorageData).not.toHaveBeenCalled(); // Set cache without tokens property (newAuthStore as any).storageDataCache = { clients: {} }; await (newAuthStore as any).clearExpiredTokens(); expect(mockStorageManager.saveStorageData).not.toHaveBeenCalled(); }); }); describe('File Watcher Management', () => { it('should handle file watcher lifecycle', async () => { const mockWatcher = { close: jest.fn() }; mockFs.existsSync.mockReturnValue(true); mockFs.watch.mockReturnValue(mockWatcher as any); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 100)); // Should set up file watcher expect(mockFs.watch).toHaveBeenCalled(); // Should close watcher on destroy newAuthStore.destroy(); expect(mockWatcher.close).toHaveBeenCalled(); }); it('should handle file watcher errors gracefully', async () => { mockFs.existsSync.mockReturnValue(true); mockFs.watch.mockImplementation(() => { throw new Error('File watcher error'); }); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); const newAuthStore = new AuthStore(); await new Promise((resolve) => setTimeout(resolve, 100)); expect(consoleSpy).toHaveBeenCalledWith('[AuthStore] Failed to setup file watcher: Error: File watcher error'); consoleSpy.mockRestore(); // Should not throw when no watcher exists expect(() => newAuthStore.destroy()).not.toThrow(); }); }); describe('Global authStore instance', () => { it('should be properly initialized', () => { expect(authStore).toBeInstanceOf(AuthStore); }); }); describe('Integration Tests', () => { it('should handle complete token and client lifecycle', async () => { const client = { client_id: 'integration-client', client_secret: 'client-secret', redirect_uris: ['http://localhost:3000/callback'], }; const token: AuthInfo = { token: 'client-token', clientId: client.client_id, scopes: ['read'], }; const appId = 'test-app'; // Complete workflow await testAuthStore.registerClient(client); await testAuthStore.storeToken(token); await testAuthStore.storeLocalAccessToken(token.token, appId); // Verify all data expect(await testAuthStore.getClient(client.client_id)).toEqual(client); expect(await testAuthStore.getToken(token.token)).toEqual(token); expect(await testAuthStore.getLocalAccessToken(appId)).toBe(token.token); // Cleanup - remove local token should also remove from tokens await testAuthStore.removeLocalAccessToken(appId); expect(await testAuthStore.getLocalAccessToken(appId)).toBeUndefined(); expect(await testAuthStore.getToken(token.token)).toBeUndefined(); // Client should still exist expect(await testAuthStore.getClient(client.client_id)).toEqual(client); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/larksuite/lark-openapi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server