Skip to main content
Glama
multi-account.test.ts8.67 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { TokenManager } from '../../../auth/tokenManager.js'; import { OAuth2Client, Credentials } from 'google-auth-library'; import fs from 'fs/promises'; // Mock the entire fs/promises module vi.mock('fs/promises'); describe('TokenManager - Multi-Account Support', () => { let oauth2Client: OAuth2Client; let tokenManager: TokenManager; beforeEach(() => { vi.clearAllMocks(); oauth2Client = new OAuth2Client('client-id', 'client-secret', 'redirect-uri'); tokenManager = new TokenManager(oauth2Client); }); describe('loadAllAccounts', () => { it('should load all accounts from token file', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh', expiry_date: Date.now() + 3600000 }, personal: { access_token: 'personal-access', refresh_token: 'personal-refresh', expiry_date: Date.now() + 3600000 } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); const accounts = await tokenManager.loadAllAccounts(); expect(accounts.size).toBe(2); expect(accounts.has('work')).toBe(true); expect(accounts.has('personal')).toBe(true); expect(accounts.get('work')).toBeInstanceOf(OAuth2Client); expect(accounts.get('personal')).toBeInstanceOf(OAuth2Client); }); it('should return empty map when no token file exists', async () => { vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' }); const accounts = await tokenManager.loadAllAccounts(); expect(accounts.size).toBe(0); }); it('should skip invalid account entries', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh' }, invalid: null, // Invalid entry personal: { access_token: 'personal-access', refresh_token: 'personal-refresh' } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); const accounts = await tokenManager.loadAllAccounts(); expect(accounts.size).toBe(2); expect(accounts.has('work')).toBe(true); expect(accounts.has('personal')).toBe(true); expect(accounts.has('invalid')).toBe(false); }); it('should validate account IDs when loading', async () => { const mockTokens = { 'valid-account': { access_token: 'valid-access', refresh_token: 'valid-refresh' }, '../../../etc/passwd': { // Invalid account ID access_token: 'bad-access', refresh_token: 'bad-refresh' } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); const accounts = await tokenManager.loadAllAccounts(); expect(accounts.size).toBe(1); expect(accounts.has('valid-account')).toBe(true); expect(accounts.has('../../../etc/passwd')).toBe(false); }); }); describe('getClient', () => { it('should return OAuth2Client for existing account', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh' } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); await tokenManager.loadAllAccounts(); const client = tokenManager.getClient('work'); expect(client).toBeInstanceOf(OAuth2Client); expect(client.credentials.access_token).toBe('work-access'); }); it('should throw error for non-existent account', async () => { vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({})); await tokenManager.loadAllAccounts(); expect(() => tokenManager.getClient('nonexistent')) .toThrow(/Account "nonexistent" not found/i); }); it('should validate account ID before lookup', () => { expect(() => tokenManager.getClient('../../../etc/passwd')) .toThrow(/Invalid account ID/i); }); }); describe('listAccounts', () => { it('should return list of account IDs with email addresses', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-access', refresh_token: 'personal-refresh' } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); // Mock getUserEmail calls vi.spyOn(tokenManager as any, 'getUserEmail') .mockResolvedValueOnce('user@company.com') .mockResolvedValueOnce('user@gmail.com'); const accounts = await tokenManager.listAccounts(); expect(accounts).toEqual([ { id: 'work', email: 'user@company.com', status: 'active', calendars: [] }, { id: 'personal', email: 'user@gmail.com', status: 'active', calendars: [] } ]); }); it('should return empty array when no accounts exist', async () => { vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' }); const accounts = await tokenManager.listAccounts(); expect(accounts).toEqual([]); }); it('should include status for each account', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh', expiry_date: Date.now() + 3600000 // Valid access token }, personal: { // No refresh_token and expired access token = truly expired access_token: 'personal-access', expiry_date: Date.now() - 3600000 // Expired } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); vi.spyOn(tokenManager as any, 'getUserEmail') .mockResolvedValue('user@example.com'); const accounts = await tokenManager.listAccounts(); // Account with refresh_token should always be active (can refresh) expect(accounts[0].status).toBe('active'); // Account without refresh_token and expired access token should be expired expect(accounts[1].status).toBe('expired'); }); it('should mark account with refresh_token as active even if access token expired', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh', expiry_date: Date.now() - 3600000 // Expired access token, but has refresh } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); vi.spyOn(tokenManager as any, 'getUserEmail') .mockResolvedValue('user@example.com'); const accounts = await tokenManager.listAccounts(); // Has refresh_token, so can get new access tokens = active expect(accounts[0].status).toBe('active'); }); }); describe('Account Isolation', () => { it('should keep tokens isolated between accounts', async () => { const mockTokens = { work: { access_token: 'work-access', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-access', refresh_token: 'personal-refresh' } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); await tokenManager.loadAllAccounts(); const workClient = tokenManager.getClient('work'); const personalClient = tokenManager.getClient('personal'); expect(workClient.credentials.access_token).toBe('work-access'); expect(personalClient.credentials.access_token).toBe('personal-access'); expect(workClient).not.toBe(personalClient); }); it('should not leak tokens when one account refreshes', async () => { const mockTokens = { work: { access_token: 'work-access-old', refresh_token: 'work-refresh' }, personal: { access_token: 'personal-access', refresh_token: 'personal-refresh' } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockTokens)); await tokenManager.loadAllAccounts(); const workClient = tokenManager.getClient('work'); const personalClient = tokenManager.getClient('personal'); // Simulate token refresh for work account workClient.setCredentials({ access_token: 'work-access-new', refresh_token: 'work-refresh' }); // Personal account should be unaffected expect(personalClient.credentials.access_token).toBe('personal-access'); }); }); });

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