Skip to main content
Glama
tokenManager-operations.test.ts8.19 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { OAuth2Client } from 'google-auth-library'; import fs from 'fs/promises'; // Mock fs/promises vi.mock('fs/promises', () => ({ default: { readFile: vi.fn(), writeFile: vi.fn(), unlink: vi.fn(), access: vi.fn(), mkdir: vi.fn() } })); // Mock auth utilities vi.mock('../../../auth/utils.js', () => ({ getSecureTokenPath: vi.fn(() => '/mock/path/tokens.json'), getAccountMode: vi.fn(() => 'normal'), getLegacyTokenPath: vi.fn(() => '/mock/legacy/tokens.json') })); // Mock paths validation vi.mock('../../../auth/paths.js', () => ({ validateAccountId: vi.fn((id: string) => { if (id.includes('@') || id.includes('..')) { throw new Error('Invalid account ID'); } return true; }) })); describe('TokenManager - removeAccount', () => { let tokenManager: any; let mockOAuth2Client: OAuth2Client; const mockedFs = fs as any; beforeEach(async () => { vi.clearAllMocks(); // Create mock OAuth2Client mockOAuth2Client = new OAuth2Client('client-id', 'client-secret', 'redirect-uri'); // Default mock implementations mockedFs.mkdir.mockResolvedValue(undefined); mockedFs.access.mockRejectedValue({ code: 'ENOENT' }); // Import TokenManager fresh for each test const { TokenManager } = await import('../../../auth/tokenManager.js'); tokenManager = new TokenManager(mockOAuth2Client); }); afterEach(() => { vi.resetModules(); }); it('should remove account from token file', async () => { // Setup: Token file with two accounts const existingTokens = { work: { access_token: 'work-token', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-token', refresh_token: 'personal-refresh' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); mockedFs.writeFile.mockResolvedValue(undefined); await tokenManager.removeAccount('work'); // Verify writeFile was called with personal only expect(mockedFs.writeFile).toHaveBeenCalled(); const writeCall = mockedFs.writeFile.mock.calls[0]; const writtenData = JSON.parse(writeCall[1]); expect(writtenData.work).toBeUndefined(); expect(writtenData.personal).toBeDefined(); }); it('should throw error if account not found', async () => { // Setup: Token file without the requested account const existingTokens = { personal: { access_token: 'personal-token', refresh_token: 'personal-refresh' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); await expect(tokenManager.removeAccount('nonexistent')) .rejects.toThrow('Account "nonexistent" not found'); }); it('should delete file if last account removed', async () => { // Setup: Token file with only one account const existingTokens = { work: { access_token: 'work-token', refresh_token: 'work-refresh' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); mockedFs.unlink.mockResolvedValue(undefined); await tokenManager.removeAccount('work'); // Verify unlink was called instead of writeFile expect(mockedFs.unlink).toHaveBeenCalledWith('/mock/path/tokens.json'); }); it('should preserve other accounts when removing one', async () => { // Setup: Token file with three accounts const existingTokens = { work: { access_token: 'work-token', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-token', refresh_token: 'personal-refresh' }, family: { access_token: 'family-token', refresh_token: 'family-refresh' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); mockedFs.writeFile.mockResolvedValue(undefined); await tokenManager.removeAccount('personal'); // Verify writeFile preserves other accounts const writeCall = mockedFs.writeFile.mock.calls[0]; const writtenData = JSON.parse(writeCall[1]); expect(writtenData.work).toBeDefined(); expect(writtenData.family).toBeDefined(); expect(writtenData.personal).toBeUndefined(); }); it('should normalize account ID to lowercase', async () => { const existingTokens = { work: { access_token: 'work-token', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-token', refresh_token: 'personal-refresh' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); mockedFs.writeFile.mockResolvedValue(undefined); // Remove with uppercase await tokenManager.removeAccount('WORK'); // Should still remove the account const writeCall = mockedFs.writeFile.mock.calls[0]; const writtenData = JSON.parse(writeCall[1]); expect(writtenData.work).toBeUndefined(); expect(writtenData.personal).toBeDefined(); }); it('should set secure file permissions (0o600)', async () => { const existingTokens = { work: { access_token: 'work-token', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-token', refresh_token: 'personal-refresh' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); mockedFs.writeFile.mockResolvedValue(undefined); await tokenManager.removeAccount('work'); // Verify writeFile was called with correct options const writeCall = mockedFs.writeFile.mock.calls[0]; expect(writeCall[2]).toEqual({ mode: 0o600 }); }); }); describe('TokenManager - listAvailableAccounts', () => { let tokenManager: any; let mockOAuth2Client: OAuth2Client; const mockedFs = fs as any; beforeEach(async () => { vi.clearAllMocks(); mockOAuth2Client = new OAuth2Client('client-id', 'client-secret', 'redirect-uri'); mockedFs.mkdir.mockResolvedValue(undefined); mockedFs.access.mockRejectedValue({ code: 'ENOENT' }); const { TokenManager } = await import('../../../auth/tokenManager.js'); tokenManager = new TokenManager(mockOAuth2Client); }); afterEach(() => { vi.resetModules(); }); it('should return list of account IDs', async () => { const existingTokens = { work: { access_token: 'work-token' }, personal: { access_token: 'personal-token' }, family: { access_token: 'family-token' } }; mockedFs.readFile.mockResolvedValue(JSON.stringify(existingTokens)); const accounts = await tokenManager.listAvailableAccounts(); expect(accounts).toContain('work'); expect(accounts).toContain('personal'); expect(accounts).toContain('family'); expect(accounts).toHaveLength(3); }); it('should return empty array when no accounts exist', async () => { mockedFs.readFile.mockRejectedValue({ code: 'ENOENT' }); const accounts = await tokenManager.listAvailableAccounts(); expect(accounts).toEqual([]); }); it('should return empty array on file read error', async () => { mockedFs.readFile.mockRejectedValue(new Error('Permission denied')); const accounts = await tokenManager.listAvailableAccounts(); expect(accounts).toEqual([]); }); }); describe('TokenManager - setAccountMode', () => { let tokenManager: any; let mockOAuth2Client: OAuth2Client; const mockedFs = fs as any; beforeEach(async () => { vi.clearAllMocks(); mockOAuth2Client = new OAuth2Client('client-id', 'client-secret', 'redirect-uri'); mockedFs.mkdir.mockResolvedValue(undefined); mockedFs.access.mockRejectedValue({ code: 'ENOENT' }); const { TokenManager } = await import('../../../auth/tokenManager.js'); tokenManager = new TokenManager(mockOAuth2Client); }); afterEach(() => { vi.resetModules(); }); it('should update account mode', () => { tokenManager.setAccountMode('work'); expect(tokenManager.getAccountMode()).toBe('work'); }); it('should allow switching between modes', () => { tokenManager.setAccountMode('work'); expect(tokenManager.getAccountMode()).toBe('work'); tokenManager.setAccountMode('personal'); expect(tokenManager.getAccountMode()).toBe('personal'); tokenManager.setAccountMode('family'); expect(tokenManager.getAccountMode()).toBe('family'); }); });

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/nspady/google-calendar-mcp'

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