import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { GdriveConfig } from '../../config/schema.js'; // Hoisted mocks so vi.mock factories can reference them const { mockFilesList, mockFilesGet, mockFilesExport, mockExistsSync, mockReadFileSync } = vi.hoisted(() => ({ mockFilesList: vi.fn(), mockFilesGet: vi.fn(), mockFilesExport: vi.fn(), mockExistsSync: vi.fn(), mockReadFileSync: vi.fn(), })); vi.mock('googleapis', () => ({ google: { auth: { OAuth2: vi.fn().mockImplementation(() => ({ setCredentials: vi.fn(), })), }, drive: vi.fn().mockReturnValue({ files: { list: mockFilesList, get: mockFilesGet, export: mockFilesExport, }, }), }, })); vi.mock('fs', async () => { const actual = await vi.importActual('fs'); return { ...actual, existsSync: mockExistsSync, readFileSync: mockReadFileSync, }; }); import { createGdriveTools } from './gdrive.js'; // ── Test config ───────────────────────────────────────────────────────────── const testConfig: NonNullable = { enabled: true, credentials_file: '/tmp/test-creds.json', token_file: '/tmp/test-token.json', }; const fakeCredentials = { installed: { client_id: 'test-client-id', client_secret: 'test-client-secret', redirect_uris: ['http://localhost'], }, }; const fakeToken = { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }; // ── Helpers ───────────────────────────────────────────────────────────────── function setupValidAuth() { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockImplementation((path: unknown) => { const p = String(path); if (p.includes('creds')) return JSON.stringify(fakeCredentials); if (p.includes('token')) return JSON.stringify(fakeToken); return ''; }); } function mockDriveFile( id: string, name: string, mimeType: string, opts?: { modifiedTime?: string; size?: string; owners?: string[]; webViewLink?: string }, ) { return { id, name, mimeType, modifiedTime: opts?.modifiedTime ?? '2026-02-10T12:00:00Z', size: opts?.size, owners: (opts?.owners ?? ['owner@test.com']).map(email => ({ emailAddress: email })), webViewLink: opts?.webViewLink ?? `https://drive.google.com/file/d/${id}/view`, }; } // ═════════════════════════════════════════════════════════════════════════════ beforeEach(() => { vi.clearAllMocks(); }); describe('createGdriveTools', () => { it('returns 3 tools with correct names', () => { const tools = createGdriveTools(testConfig); expect(tools).toHaveLength(3); expect(tools.map(t => t.name)).toEqual(['drive.list', 'drive.search', 'drive.read']); }); it('tools have descriptions and input schemas', () => { const tools = createGdriveTools(testConfig); for (const tool of tools) { expect(tool.description).toBeTruthy(); expect(tool.inputSchema).toBeDefined(); expect(tool.inputSchema.type).toBe('object'); } }); }); describe('drive.list', () => { it('returns error when credentials file missing', async () => { mockExistsSync.mockReturnValue(false); const [listTool] = createGdriveTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('Credentials file not found'); }); it('returns error when token file missing', async () => { mockExistsSync.mockImplementation((path: unknown) => { return String(path).includes('creds'); }); mockReadFileSync.mockReturnValue(JSON.stringify(fakeCredentials)); const [listTool] = createGdriveTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('Token file not found'); }); it('lists recent files', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [ mockDriveFile('f1', 'Report.pdf', 'application/pdf', { size: '1048576' }), mockDriveFile('f2', 'Notes', 'application/vnd.google-apps.document'), ], }, }); const [listTool] = createGdriveTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(true); expect(result.output).toContain('Report.pdf'); expect(result.output).toContain('PDF'); expect(result.output).toContain('1.0 MB'); expect(result.output).toContain('Notes'); expect(result.output).toContain('Google Doc'); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ q: 'trashed=false', pageSize: 10, orderBy: 'modifiedTime desc', }), ); }); it('handles empty results', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [] } }); const [listTool] = createGdriveTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(true); expect(result.output).toBe('No files found.'); }); it('respects maxResults parameter', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [] } }); const [listTool] = createGdriveTools(testConfig); await listTool.execute({ maxResults: 5 }); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ pageSize: 5 }), ); }); it('filters by mimeType', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [] } }); const [listTool] = createGdriveTools(testConfig); await listTool.execute({ mimeType: 'application/pdf' }); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ q: "trashed=false and mimeType='application/pdf'", }), ); }); it('filters by folderId', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [] } }); const [listTool] = createGdriveTools(testConfig); await listTool.execute({ folderId: 'folder123' }); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ q: "trashed=false and 'folder123' in parents", }), ); }); it('handles API errors gracefully', async () => { setupValidAuth(); mockFilesList.mockRejectedValue(new Error('API quota exceeded')); const [listTool] = createGdriveTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('API quota exceeded'); }); }); describe('drive.search', () => { it('searches with query parameter', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [ mockDriveFile('f1', 'Q1 Budget', 'application/vnd.google-apps.spreadsheet'), ], }, }); const [, searchTool] = createGdriveTools(testConfig); const result = await searchTool.execute({ query: 'budget' }); expect(result.success).toBe(true); expect(result.output).toContain('Q1 Budget'); expect(result.output).toContain('Google Sheet'); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ q: expect.stringContaining("name contains 'budget'"), }), ); }); it('escapes single quotes in query', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [] } }); const [, searchTool] = createGdriveTools(testConfig); await searchTool.execute({ query: "it's a test" }); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ q: expect.stringContaining("it\\'s a test"), }), ); }); it('filters by mimeType', async () => { setupValidAuth(); mockFilesList.mockResolvedValue({ data: { files: [] } }); const [, searchTool] = createGdriveTools(testConfig); await searchTool.execute({ query: 'report', mimeType: 'application/pdf' }); expect(mockFilesList).toHaveBeenCalledWith( expect.objectContaining({ q: expect.stringContaining("mimeType='application/pdf'"), }), ); }); it('handles API errors gracefully', async () => { setupValidAuth(); mockFilesList.mockRejectedValue(new Error('API quota exceeded')); const [, searchTool] = createGdriveTools(testConfig); const result = await searchTool.execute({ query: 'test' }); expect(result.success).toBe(false); expect(result.error).toContain('API quota exceeded'); }); }); describe('drive.read', () => { it('exports Google Docs as plain text', async () => { setupValidAuth(); mockFilesGet.mockResolvedValue({ data: { id: 'doc1', name: 'My Doc', mimeType: 'application/vnd.google-apps.document', size: null }, }); mockFilesExport.mockResolvedValue({ data: 'Hello from the document' }); const [, , readTool] = createGdriveTools(testConfig); const result = await readTool.execute({ fileId: 'doc1' }); expect(result.success).toBe(true); expect(result.output).toContain('Name: My Doc'); expect(result.output).toContain('Type: Google Doc'); expect(result.output).toContain('Hello from the document'); expect(mockFilesExport).toHaveBeenCalledWith( { fileId: 'doc1', mimeType: 'text/plain' }, { responseType: 'text' }, ); }); it('exports Google Sheets as CSV', async () => { setupValidAuth(); mockFilesGet.mockResolvedValue({ data: { id: 'sheet1', name: 'Budget', mimeType: 'application/vnd.google-apps.spreadsheet' }, }); mockFilesExport.mockResolvedValue({ data: 'Name,Amount\nAlice,100' }); const [, , readTool] = createGdriveTools(testConfig); const result = await readTool.execute({ fileId: 'sheet1' }); expect(result.success).toBe(true); expect(result.output).toContain('Name: Budget'); expect(result.output).toContain('Type: Google Sheet'); expect(result.output).toContain('Name,Amount'); expect(mockFilesExport).toHaveBeenCalledWith( { fileId: 'sheet1', mimeType: 'text/csv' }, { responseType: 'text' }, ); }); it('downloads plain text files directly', async () => { setupValidAuth(); mockFilesGet .mockResolvedValueOnce({ data: { id: 'txt1', name: 'notes.txt', mimeType: 'text/plain', size: '42' }, }) .mockResolvedValueOnce({ data: 'These are my notes' }); const [, , readTool] = createGdriveTools(testConfig); const result = await readTool.execute({ fileId: 'txt1' }); expect(result.success).toBe(true); expect(result.output).toContain('Name: notes.txt'); expect(result.output).toContain('These are my notes'); }); it('reports binary files as unreadable', async () => { setupValidAuth(); mockFilesGet.mockResolvedValue({ data: { id: 'img1', name: 'photo.jpg', mimeType: 'image/jpeg', size: '500000' }, }); const [, , readTool] = createGdriveTools(testConfig); const result = await readTool.execute({ fileId: 'img1' }); expect(result.success).toBe(true); expect(result.output).toContain('Name: photo.jpg'); expect(result.output).toContain('Binary file'); }); it('returns error when credentials missing', async () => { mockExistsSync.mockReturnValue(false); const [, , readTool] = createGdriveTools(testConfig); const result = await readTool.execute({ fileId: 'doc1' }); expect(result.success).toBe(false); expect(result.error).toContain('Credentials file not found'); }); it('handles API errors gracefully', async () => { setupValidAuth(); mockFilesGet.mockRejectedValue(new Error('File not found')); const [, , readTool] = createGdriveTools(testConfig); const result = await readTool.execute({ fileId: 'nonexistent' }); expect(result.success).toBe(false); expect(result.error).toContain('File not found'); }); });