Skip to main content
Glama

Firebase MCP

firestoreClient.test.ts55.7 kB
import { getDocument, updateDocument, deleteDocument, addDocument, listDocuments, list_collections, queryCollectionGroup, } from '../firestoreClient'; import { admin } from '../firebaseConfig'; import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; // Test imports for mocking import * as firebaseConfig from '../firebaseConfig'; /** * Firestore Client Tests * * These tests verify the functionality of the Firestore client operations. * Tests run against the Firebase emulator when available. */ // Test collection and document data const testCollection = 'test_collection'; const testDocId = 'test-doc-id'; const testData = { name: 'Test Document', timestamp: new Date().toISOString().split('T')[0], }; // Helper function to ensure test document exists async function ensureTestDocument() { try { const docRef = admin.firestore().collection(testCollection).doc(testDocId); await docRef.set(testData); console.log('[TEST DEBUG]', 'Test document created/updated:', testDocId); } catch (error) { console.error('[TEST ERROR]', 'Error ensuring test document exists:', error); } } // Helper function to delete test document async function deleteTestDocument() { try { await admin.firestore().collection(testCollection).doc(testDocId).delete(); console.log('[TEST DEBUG]', 'Test document deleted:', testDocId); } catch (_error) { // Ignore errors if document doesn't exist } } // Set up test environment beforeAll(async () => { // Ensure we're using the emulator in test mode if (process.env.USE_FIREBASE_EMULATOR === 'true') { process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'; console.log('[TEST DEBUG]', 'Using Firestore emulator'); } await ensureTestDocument(); }); // Clean up after tests afterAll(async () => { await deleteTestDocument(); }); // Reset mocks between tests beforeEach(() => { vi.restoreAllMocks(); }); describe('Firestore Client', () => { describe('getDocument', () => { // Test getting an existing document it('should return document data when valid ID is provided', async () => { const result = await getDocument(testCollection, testDocId); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify document data structure expect(responseData.id).toBe(testDocId); expect(responseData.data).toEqual(testData); expect(responseData.url).toBeDefined(); }); // Test error handling for non-existent document it('should handle non-existent document gracefully', async () => { const result = await getDocument(testCollection, 'non-existent-doc'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Document not found: non-existent-doc' }) ); }); // Test error handling when getProjectId returns null it('should handle null project ID gracefully', async () => { // Save service account path to restore later const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Mock getProjectId to return null for this test only vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue(null); // Set service account path to ensure code path is executed process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Call the function const result = await getDocument(testCollection, testDocId); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Could not determine project ID' }) ); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test error handling when SERVICE_ACCOUNT_KEY_PATH is not set it('should handle missing service account path for getDocument', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Clear the service account path delete process.env.SERVICE_ACCOUNT_KEY_PATH; // Call the function const result = await getDocument(testCollection, testDocId); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Service account path not set' }) ); } finally { // Restore the original service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test for error handling in getDocument (lines 287-292) it('should handle Firestore errors in getDocument', async () => { // Mock admin.firestore().collection().doc().get() to throw an error const mockGet = vi.fn().mockRejectedValue(new Error('Firestore error')); const mockDoc = vi.fn().mockReturnValue({ get: mockGet, }); const mockCollection = vi.fn().mockReturnValue({ doc: mockDoc, }); vi.spyOn(admin.firestore(), 'collection').mockImplementation(mockCollection); // Call the function const result = await getDocument('testCollection', 'nonexistent-id'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Firestore error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in getDocument when Firebase is not initialized (line 286) it('should handle Firebase initialization errors in getDocument', async () => { // Mock admin.firestore to throw an error vi.spyOn(admin, 'firestore').mockImplementation(() => { throw new Error('Firebase not initialized'); }); // Call the function const result = await getDocument('testCollection', 'test-id'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: 'Firebase not initialized' })); // Restore the original implementation vi.restoreAllMocks(); }); }); describe('addDocument', () => { // Test adding a new document it('should add a document and return its ID', async () => { const newDocData = { name: 'New Test Document', timestamp: new Date().toISOString().split('T')[0], }; const result = await addDocument(testCollection, newDocData); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify document data structure expect(responseData.id).toBeDefined(); expect(responseData.url).toBeDefined(); // Clean up the added document if (responseData.id) { await admin.firestore().collection(testCollection).doc(responseData.id).delete(); } }); // Test error handling when SERVICE_ACCOUNT_KEY_PATH is not set it('should handle missing service account path for addDocument', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Clear the service account path delete process.env.SERVICE_ACCOUNT_KEY_PATH; // Data for new document const newDocData = { name: 'Test Doc', timestamp: new Date().toISOString(), }; // Call the function const result = await addDocument(testCollection, newDocData); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Service account path not set' }) ); } finally { // Restore the original service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test error handling when getProjectId returns null it('should handle null project ID gracefully for addDocument', async () => { // Save service account path to restore later const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Mock getProjectId to return null for this test only vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue(null); // Set service account path to ensure code path is executed process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Data for new document const newDocData = { name: 'Test Doc', timestamp: new Date().toISOString(), }; // Call the function const result = await addDocument(testCollection, newDocData); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Could not determine project ID' }) ); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test for error handling in addDocument (lines 233-238) it('should handle Firestore errors in addDocument', async () => { // Mock admin.firestore().collection().add() to throw an error const mockAdd = vi.fn().mockRejectedValue(new Error('Firestore add error')); const mockCollection = vi.fn().mockReturnValue({ add: mockAdd, }); vi.spyOn(admin.firestore(), 'collection').mockImplementation(mockCollection); // Call the function const result = await addDocument('testCollection', { test: true }); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Firestore add error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in addDocument when Firebase is not initialized (line 232) it('should handle Firebase initialization errors in addDocument', async () => { // Mock admin.firestore to throw an error vi.spyOn(admin, 'firestore').mockImplementation(() => { throw new Error('Firebase not initialized'); }); // Call the function const result = await addDocument('testCollection', { test: true }); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: 'Firebase not initialized' })); // Restore the original implementation vi.restoreAllMocks(); }); }); describe('updateDocument', () => { // Test updating an existing document it('should update document data when valid ID and data are provided', async () => { const updateData = { name: 'Updated Document', updated: true, }; const result = await updateDocument(testCollection, testDocId, updateData); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify update response expect(responseData.success).toBe(true); expect(responseData.url).toBeDefined(); // Verify the document was actually updated const updatedDoc = await getDocument(testCollection, testDocId); const updatedData = JSON.parse(updatedDoc.content[0].text).data; expect(updatedData.name).toBe(updateData.name); expect(updatedData.updated).toBe(true); }); // Test error handling for non-existent document it('should handle updating non-existent document gracefully', async () => { const result = await updateDocument(testCollection, 'non-existent-doc', { test: true }); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toMatch(/NOT_FOUND/); }); // Test error handling when service account path is not set it('should handle Firestore errors when service account path is not set', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Clear the service account path delete process.env.SERVICE_ACCOUNT_KEY_PATH; // Create a temporary document that we know exists const tempDocRef = admin.firestore().collection(testCollection).doc(testDocId); await tempDocRef.set({ temp: true }); // Call the function const result = await updateDocument(testCollection, testDocId, { test: true }); // The update succeeds but then fails when trying to get the project ID for the console URL expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Service account path not set' }) ); } finally { // Restore the original service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test error handling when getProjectId returns null it('should handle null project ID gracefully for updateDocument', async () => { // Save service account path to restore later const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Mock getProjectId to return null for this test only vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue(null); // Set service account path to ensure code path is executed process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Data for document update const updateData = { name: 'Updated Test Doc', timestamp: new Date().toISOString(), }; // Call the function const result = await updateDocument(testCollection, testDocId, updateData); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Could not determine project ID' }) ); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test for error handling in updateDocument when Firestore update fails (line 339) it('should handle Firestore errors in updateDocument', async () => { // Mock admin.firestore().collection().doc().update() to throw an error const mockUpdate = vi.fn().mockRejectedValue(new Error('Firestore update error')); const mockDoc = vi.fn().mockReturnValue({ update: mockUpdate, }); const mockCollection = vi.fn().mockReturnValue({ doc: mockDoc, }); vi.spyOn(admin.firestore(), 'collection').mockImplementation(mockCollection); // Call the function const result = await updateDocument('testCollection', 'test-id', { test: true }); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Firestore update error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in updateDocument when Firebase is not initialized (line 339) it('should handle Firebase initialization errors in updateDocument', async () => { // Mock admin.firestore to throw an error vi.spyOn(admin, 'firestore').mockImplementation(() => { throw new Error('Firebase not initialized'); }); // Call the function const result = await updateDocument('testCollection', 'test-id', { test: true }); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: 'Firebase not initialized' })); // Restore the original implementation vi.restoreAllMocks(); }); }); describe('deleteDocument', () => { it('should delete a document in a collection', async () => { // First create a document to delete const tempDocId = 'temp-doc-to-delete'; await admin.firestore().collection(testCollection).doc(tempDocId).set(testData); const result = await deleteDocument(testCollection, tempDocId); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify delete response expect(responseData.success).toBe(true); // Verify the document was actually deleted const deletedDoc = await getDocument(testCollection, tempDocId); expect(deletedDoc.isError).toBe(true); expect(deletedDoc.content[0].text).toBe( JSON.stringify({ error: 'Document not found: ' + tempDocId }) ); }); it('should report firestore errors when service account path is not set', async () => { // Create proper mocks for Firestore methods const mockDocData = { exists: false }; // Create mock document reference with both get and delete methods const mockDoc = { get: vi.fn().mockResolvedValue(mockDocData), delete: vi.fn(), }; const mockCollection = { doc: vi.fn().mockReturnValue(mockDoc), }; // Mock the collection method vi.spyOn(admin.firestore(), 'collection').mockReturnValue(mockCollection as any); const result = await deleteDocument('tests', 'non-existent-doc'); // For non-existent documents, expect the "no entity to delete" message expect(result).toHaveProperty('content'); expect(result.content[0].text).toBe(JSON.stringify({ error: 'no entity to delete' })); expect(result.isError).toBe(true); }); // Test for handling Firebase errors it('should handle Firebase errors in deleteDocument', async () => { // Mock Firestore to throw an error const mockError = new Error('Firebase error during delete'); const mockDoc = { get: vi.fn().mockRejectedValue(mockError), delete: vi.fn(), }; const mockCollection = { doc: vi.fn().mockReturnValue(mockDoc), }; // Mock the collection method vi.spyOn(admin.firestore(), 'collection').mockReturnValue(mockCollection as any); const result = await deleteDocument('tests', 'error-doc'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: mockError.message })); }); // Test for error handling in deleteDocument when Firebase is not initialized (line 376) it('should handle Firebase initialization errors in deleteDocument', async () => { // Mock admin.firestore to throw an error vi.spyOn(admin, 'firestore').mockImplementation(() => { throw new Error('Firebase not initialized'); }); // Call the function const result = await deleteDocument('testCollection', 'test-id'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: 'Firebase not initialized' })); // Restore the original implementation vi.restoreAllMocks(); }); }); describe('listDocuments', () => { it('should list documents with filters', async () => { // First ensure we have a document with the expected timestamp // This creates a new document with the exact timestamp we're going to filter on const docWithFilteredTimestamp = { name: 'Filtered Test Document', timestamp: testData.timestamp, }; // Add the document to ensure it exists const addResult = await addDocument(testCollection, docWithFilteredTimestamp); const responseData = JSON.parse(addResult.content[0].text); const addedDocId = responseData.id; try { // Create the filter with the same timestamp const dateFilter = { field: 'timestamp', operator: '==' as const, value: testData.timestamp, }; const result = await listDocuments(testCollection, [dateFilter]); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const listResponseData = JSON.parse(result.content[0].text); // Verify documents array exists expect(Array.isArray(listResponseData.documents)).toBe(true); expect(listResponseData.documents.length).toBeGreaterThan(0); // Verify document structure const document = listResponseData.documents[0]; expect(document.id).toBeDefined(); expect(document.data).toBeDefined(); expect(document.url).toBeDefined(); expect(document.data.timestamp).toBe(testData.timestamp); } finally { // Clean up - remove the document we added for this test if (addedDocId) { await admin.firestore().collection(testCollection).doc(addedDocId).delete(); } } }); // Test error handling for Firebase initialization issues it('should handle Firebase initialization issues', async () => { // Use vi.spyOn to mock the admin.firestore method const firestoreSpy = vi.spyOn(admin, 'firestore').mockImplementation(() => { throw new Error('Firebase not initialized'); }); try { const result = await listDocuments(testCollection, []); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: 'Firebase not initialized' })); } finally { // Restore the original implementation firestoreSpy.mockRestore(); } }); // Test error handling when SERVICE_ACCOUNT_KEY_PATH is not set it('should handle missing service account path for listDocuments', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Clear the service account path delete process.env.SERVICE_ACCOUNT_KEY_PATH; // Call the function const result = await listDocuments(testCollection); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Service account path not set' }) ); } finally { // Restore the original service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test error handling when getProjectId returns null it('should handle null project ID gracefully for listDocuments', async () => { // Save service account path to restore later const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Mock getProjectId to return null for this test only vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue(null); // Set service account path to ensure code path is executed process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Call the function const result = await listDocuments(testCollection); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Could not determine project ID' }) ); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test pagination with pageToken it('should handle pagination with pageToken', async () => { // Create multiple documents to ensure pagination const docIds: string[] = []; const batchSize = 5; try { // Create batch of test documents for (let i = 0; i < batchSize; i++) { const docData = { name: `Pagination Test Document ${i}`, index: i, timestamp: testData.timestamp, }; const addResult = await addDocument(testCollection, docData); const responseData = JSON.parse(addResult.content[0].text); docIds.push(responseData.id); } // First page - get 2 documents const firstPageResult = await listDocuments(testCollection, undefined, 2); const firstPageData = JSON.parse(firstPageResult.content[0].text); // Verify first page expect(firstPageData.documents.length).toBe(2); expect(firstPageData.nextPageToken).toBeDefined(); // Second page - use the pageToken from first page const secondPageResult = await listDocuments( testCollection, undefined, 2, firstPageData.nextPageToken ); const secondPageData = JSON.parse(secondPageResult.content[0].text); // Verify second page expect(secondPageData.documents.length).toBe(2); expect(secondPageData.documents[0].id).not.toBe(firstPageData.documents[0].id); expect(secondPageData.documents[1].id).not.toBe(firstPageData.documents[1].id); } finally { // Clean up test documents for (const docId of docIds) { await admin.firestore().collection(testCollection).doc(docId).delete(); } } }); // Test handling of invalid pageToken it('should handle invalid pageToken gracefully', async () => { const result = await listDocuments(testCollection, undefined, 5, 'invalid/document/path'); // Invalid document path will result in an error expect(result.isError).toBe(true); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); }); // Test for error handling in listDocuments when Firestore query fails (lines 77-82) it('should handle Firestore query errors in listDocuments', async () => { // Mock admin.firestore().collection().get to throw an error const mockGet = vi.fn().mockRejectedValue(new Error('Firestore query error')); const mockWhere = vi.fn().mockReturnThis(); const mockOrderBy = vi.fn().mockReturnThis(); const mockLimit = vi.fn().mockReturnThis(); const mockStartAfter = vi.fn().mockReturnThis(); const mockCollection = vi.fn().mockReturnValue({ where: mockWhere, orderBy: mockOrderBy, limit: mockLimit, startAfter: mockStartAfter, get: mockGet, }); vi.spyOn(admin.firestore(), 'collection').mockImplementation(mockCollection); // Call the function const result = await listDocuments('testCollection'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Firestore query error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in listDocuments when doc() throws an error (lines 144-149) it('should handle doc() errors in listDocuments with pageToken', async () => { // Mock admin.firestore().doc() to throw an error const mockDoc = vi.fn().mockImplementation(() => { throw new Error('Invalid document reference'); }); vi.spyOn(admin.firestore(), 'doc').mockImplementation(mockDoc); // Call the function with an invalid pageToken const result = await listDocuments('testCollection', undefined, 5, 'invalid/doc/path'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid document reference'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for empty results in listDocuments (line 177) it('should handle empty results in listDocuments', async () => { // Mock admin.firestore().collection().get() to return empty results const mockGet = vi.fn().mockResolvedValue({ docs: [] }); const mockWhere = vi.fn().mockReturnThis(); const mockLimit = vi.fn().mockReturnThis(); const mockStartAfter = vi.fn().mockReturnThis(); const mockCollection = vi.fn().mockReturnValue({ where: mockWhere, limit: mockLimit, startAfter: mockStartAfter, get: mockGet, }); vi.spyOn(admin.firestore(), 'collection').mockImplementation(mockCollection); // Set service account path to ensure code path is executed const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Mock getProjectId to return a valid project ID vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue('test-project'); try { // Call the function const result = await listDocuments('testCollection'); // Verify response expect(result.isError).toBeUndefined(); expect(result.content[0].type).toBe('text'); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify documents array exists and is empty expect(Array.isArray(responseData.documents)).toBe(true); expect(responseData.documents.length).toBe(0); // Verify nextPageToken is undefined expect(responseData.nextPageToken).toBeUndefined(); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; vi.restoreAllMocks(); } }); }); describe('listCollections', () => { it('should list available collections', async () => { const result = await list_collections(); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify collections array exists expect(Array.isArray(responseData.collections)).toBe(true); // Verify collection structure const collection = responseData.collections[0]; expect(collection.id).toBeDefined(); expect(collection.path).toBeDefined(); expect(collection.url).toBeDefined(); }); // Test error handling when getProjectId returns null it('should handle null project ID gracefully for list_collections', async () => { // Save service account path to restore later const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Mock getProjectId to return null for this test only vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue(null); // Set service account path to ensure code path is executed process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Call the function const result = await list_collections(); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Could not determine project ID' }) ); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test error handling when SERVICE_ACCOUNT_KEY_PATH is not set it('should handle missing service account path for list_collections', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Clear the service account path delete process.env.SERVICE_ACCOUNT_KEY_PATH; // Call the function const result = await list_collections(); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Service account path not set' }) ); } finally { // Restore the original service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test listing subcollections of a document it('should list subcollections of a document when documentPath is provided', async () => { // First create a document with a subcollection const parentDocId = 'parent-doc-with-subcollection'; const subcollectionName = 'test_subcollection'; const subcollectionDocId = 'subtest-doc'; try { // Create parent document await admin.firestore().collection(testCollection).doc(parentDocId).set({ name: 'Parent Document', }); // Create a document in a subcollection await admin .firestore() .collection(testCollection) .doc(parentDocId) .collection(subcollectionName) .doc(subcollectionDocId) .set({ name: 'Subcollection Document' }); // List subcollections const documentPath = `${testCollection}/${parentDocId}`; const result = await list_collections(documentPath); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify collections array exists expect(Array.isArray(responseData.collections)).toBe(true); // The subcollection should be in the result const foundSubcollection = responseData.collections.find( (col: any) => col.id === subcollectionName ); expect(foundSubcollection).toBeDefined(); expect(foundSubcollection.path).toContain(parentDocId); expect(foundSubcollection.url).toBeDefined(); } finally { // Clean up - delete the test documents try { await admin .firestore() .collection(testCollection) .doc(parentDocId) .collection(subcollectionName) .doc(subcollectionDocId) .delete(); await admin.firestore().collection(testCollection).doc(parentDocId).delete(); } catch (error) { console.error('Cleanup error:', error); } } }); // Test for error handling in list_collections (lines 77-82) it('should handle Firestore errors in list_collections', async () => { // Mock admin.firestore().listCollections() to throw an error const mockListCollections = vi .fn() .mockRejectedValue(new Error('Firestore list collections error')); vi.spyOn(admin.firestore(), 'listCollections').mockImplementation(mockListCollections); // Call the function const result = await list_collections(); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Firestore list collections error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in list_collections when Firebase is not initialized (line 77) it('should handle Firebase initialization errors in list_collections', async () => { // Mock admin.firestore to throw an error vi.spyOn(admin, 'firestore').mockImplementation(() => { throw new Error('Firebase not initialized'); }); // Call the function const result = await list_collections(); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe(JSON.stringify({ error: 'Firebase not initialized' })); // Restore the original implementation vi.restoreAllMocks(); }); }); describe('queryCollectionGroup', () => { it('should query documents across subcollections with the same name', async () => { // Create parent documents const parentDoc1Id = 'parent-doc-1'; const parentDoc2Id = 'parent-doc-2'; const subcollectionName = 'comments'; try { // Create parent documents await admin.firestore().collection(testCollection).doc(parentDoc1Id).set({ name: 'Parent Document 1', }); await admin.firestore().collection(testCollection).doc(parentDoc2Id).set({ name: 'Parent Document 2', }); // Create documents in subcollections await admin .firestore() .collection(testCollection) .doc(parentDoc1Id) .collection(subcollectionName) .doc('comment1') .set({ text: 'Comment 1 on parent 1', author: 'User A', timestamp: new Date('2023-01-01'), }); await admin .firestore() .collection(testCollection) .doc(parentDoc2Id) .collection(subcollectionName) .doc('comment2') .set({ text: 'Comment 1 on parent 2', author: 'User B', timestamp: new Date('2023-01-02'), }); // Query the collection group const result = await queryCollectionGroup(subcollectionName); // Print debug info from first test console.log('[TEST DEBUG] First test - result.content:', result.content); console.log('[TEST DEBUG] First test - content type:', result.content[0].type); console.log('[TEST DEBUG] First test - content text type:', typeof result.content[0].text); console.log('[TEST DEBUG] First test - text content:', result.content[0].text); // Verify the response format expect(result.content).toBeDefined(); expect(result.content.length).toBe(1); // Parse the response const responseData = JSON.parse(result.content[0].text); // Verify documents array exists expect(Array.isArray(responseData.documents)).toBe(true); // Instead of checking total count, filter to only include our test documents const testDocuments = responseData.documents.filter( (doc: any) => doc.path.includes(`${testCollection}/${parentDoc1Id}/${subcollectionName}`) || doc.path.includes(`${testCollection}/${parentDoc2Id}/${subcollectionName}`) ); // Verify we have our 2 test documents expect(testDocuments.length).toBe(2); // Verify both documents are returned with correct data const document1 = testDocuments.find((doc: any) => doc.id === 'comment1'); const document2 = testDocuments.find((doc: any) => doc.id === 'comment2'); expect(document1).toBeDefined(); expect(document1.data.author).toBe('User A'); expect(document1.path).toContain( `${testCollection}/${parentDoc1Id}/${subcollectionName}/comment1` ); expect(document2).toBeDefined(); expect(document2.data.author).toBe('User B'); expect(document2.path).toContain( `${testCollection}/${parentDoc2Id}/${subcollectionName}/comment2` ); } finally { // Clean up - delete the test documents try { await admin .firestore() .collection(testCollection) .doc(parentDoc1Id) .collection(subcollectionName) .doc('comment1') .delete(); await admin .firestore() .collection(testCollection) .doc(parentDoc2Id) .collection(subcollectionName) .doc('comment2') .delete(); await admin.firestore().collection(testCollection).doc(parentDoc1Id).delete(); await admin.firestore().collection(testCollection).doc(parentDoc2Id).delete(); } catch (error) { console.error('Cleanup error:', error); } } }); it('should query collection group with filters', async () => { // Check for skip condition if (!process.env.SERVICE_ACCOUNT_KEY_PATH) { console.log('TEST INFO: Skipping test due to missing SERVICE_ACCOUNT_KEY_PATH'); return; } const collectionGroup = 'testSubcollection'; const filters = [{ field: 'testField', operator: '==' as const, value: 'testValue' }]; try { const result = await queryCollectionGroup(collectionGroup, filters); console.log('DEBUG_TEST: Result object type:', typeof result); if (result && typeof result === 'object') { console.log('DEBUG_TEST: Result has these keys:', Object.keys(result)); if ('content' in result) { console.log('DEBUG_TEST: Content type:', typeof result.content); console.log('DEBUG_TEST: Content:', result.content); if (Array.isArray(result.content) && result.content.length > 0) { const firstContent = result.content[0]; console.log('DEBUG_TEST: First content item type:', typeof firstContent); console.log('DEBUG_TEST: First content item keys:', Object.keys(firstContent)); if (firstContent && typeof firstContent === 'object' && 'text' in firstContent) { console.log('DEBUG_TEST: Text type:', typeof firstContent.text); console.log( 'DEBUG_TEST: First 200 chars of text:', firstContent.text.substring(0, 200) ); try { const parsed = JSON.parse(firstContent.text); console.log('DEBUG_TEST: Successfully parsed content. Result:', parsed); if (parsed && parsed.documents) { console.log('DEBUG_TEST: Documents array length:', parsed.documents.length); } } catch (parseError: any) { console.log('DEBUG_TEST: Parse error details:', parseError.message); if (typeof firstContent.text === 'string') { console.log( 'DEBUG_TEST: Character at position 1:', firstContent.text.charCodeAt(1) ); console.log( 'DEBUG_TEST: Character at position 2:', firstContent.text.charCodeAt(2) ); console.log( 'DEBUG_TEST: Characters 0-5:', Array.from(firstContent.text.substring(0, 5)).map((c: string) => c.charCodeAt(0) ) ); } } } } } } expect(result).toBeDefined(); // Our improved error handling doesn't set isError flag but includes error property in the response // expect(result.isError).toBeTruthy(); expect(result.content).toBeTruthy(); if (Array.isArray(result.content) && result.content.length > 0) { // Skip JSON parsing and just check that content exists expect(result.content[0].text).toBeDefined(); // Try to parse only if it looks like valid JSON if (result.content[0].text.trim().startsWith('{')) { try { const responseData = JSON.parse(result.content[0].text); expect(responseData).toBeDefined(); // Check for either documents array or error property if (responseData.error) { // If error response, verify it has the expected format expect(responseData.error).toBeDefined(); if (responseData.indexUrl) { expect(responseData.indexUrl).toContain('console.firebase.google.com'); } } else if (responseData.documents) { expect(Array.isArray(responseData.documents)).toBe(true); } } catch (_e) { // If parsing fails, just verify we have content expect(typeof result.content[0].text).toBe('string'); } } else { // If not JSON format, just verify text exists expect(typeof result.content[0].text).toBe('string'); } } else { expect(Array.isArray(result.content) && result.content.length > 0).toBe(true); } } catch (error) { console.log('DEBUG_TEST: Caught error in test:', error); throw error; } }); // Test for invalid pageToken handling (lines 435-451) it('should handle invalid pageToken in queryCollectionGroup', async () => { // Mock admin.firestore().doc to throw an error const mockDoc = vi.fn().mockImplementation(() => { throw new Error('Invalid document path'); }); vi.spyOn(admin.firestore(), 'doc').mockImplementation(mockDoc); // Call the function with an invalid pageToken const result = await queryCollectionGroup( 'testCollection', undefined, undefined, 10, 'invalid/token' ); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid pagination token'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for missing service account path (lines 459-463) it('should handle missing service account path in queryCollectionGroup', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Clear the service account path delete process.env.SERVICE_ACCOUNT_KEY_PATH; // Call the function const result = await queryCollectionGroup('testCollection'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Service account path not set' }) ); } finally { // Restore the original service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; } }); // Test for null project ID (lines 467-471) it('should handle null project ID in queryCollectionGroup', async () => { // Save the original service account path const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; try { // Mock getProjectId to return null vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue(null); // Set service account path to ensure code path is executed process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Call the function const result = await queryCollectionGroup('testCollection'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toBe( JSON.stringify({ error: 'Could not determine project ID' }) ); } finally { // Restore service account path process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; vi.restoreAllMocks(); } }); // Test for error handling in queryCollectionGroup when collection group query fails (lines 437-439) it('should handle collection group query errors', async () => { // Mock admin.firestore().collectionGroup().get to throw an error const mockGet = vi.fn().mockRejectedValue(new Error('Collection group query failed')); const mockCollectionGroup = vi.fn().mockReturnValue({ where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), startAfter: vi.fn().mockReturnThis(), get: mockGet, }); vi.spyOn(admin.firestore(), 'collectionGroup').mockImplementation(mockCollectionGroup); // Call the function const result = await queryCollectionGroup('testCollection'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Collection group query failed'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in queryCollectionGroup with orderBy (lines 428-431) it('should handle orderBy in queryCollectionGroup', async () => { // Mock admin.firestore().collectionGroup().orderBy().get to throw an error const mockGet = vi.fn().mockRejectedValue(new Error('OrderBy error')); const mockOrderBy = vi.fn().mockReturnValue({ limit: vi.fn().mockReturnThis(), get: mockGet, }); const mockCollectionGroup = vi.fn().mockReturnValue({ where: vi.fn().mockReturnThis(), orderBy: mockOrderBy, }); vi.spyOn(admin.firestore(), 'collectionGroup').mockImplementation(mockCollectionGroup); // Call the function with orderBy const orderByParams = [{ field: 'timestamp', direction: 'desc' as const }]; const result = await queryCollectionGroup('testCollection', undefined, orderByParams); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('OrderBy error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in queryCollectionGroup when query fails (lines 437-439) it('should handle query execution errors in queryCollectionGroup', async () => { // Mock admin.firestore().collectionGroup().where().get to throw an error const mockGet = vi.fn().mockRejectedValue(new Error('Query execution error')); const mockWhere = vi.fn().mockReturnValue({ orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), get: mockGet, }); const mockCollectionGroup = vi.fn().mockReturnValue({ where: mockWhere, }); vi.spyOn(admin.firestore(), 'collectionGroup').mockImplementation(mockCollectionGroup); // Call the function with filters const filters = [{ field: 'test', operator: '==' as const, value: 'value' }]; const result = await queryCollectionGroup('testCollection', filters); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Query execution error'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in queryCollectionGroup with invalid pageToken (lines 434-450) it('should handle invalid pageToken in queryCollectionGroup', async () => { // Mock admin.firestore().doc() to throw an error const mockGet = vi.fn().mockResolvedValue({ exists: false }); const mockDoc = vi.fn().mockReturnValue({ get: mockGet, }); vi.spyOn(admin.firestore(), 'doc').mockImplementation(mockDoc); // Call the function with an invalid pageToken const result = await queryCollectionGroup( 'testCollection', undefined, undefined, 20, 'invalid/doc/path' ); // Verify the document doesn't exist and startAfter isn't called expect(mockDoc).toHaveBeenCalledWith('invalid/doc/path'); expect(mockGet).toHaveBeenCalled(); // Restore the original implementation vi.restoreAllMocks(); }); // Test for error handling in queryCollectionGroup when doc() throws an error (lines 435-449) it('should handle doc() errors in queryCollectionGroup with pageToken', async () => { // Mock admin.firestore().doc() to throw an error const mockDoc = vi.fn().mockImplementation(() => { throw new Error('Invalid document reference'); }); vi.spyOn(admin.firestore(), 'doc').mockImplementation(mockDoc); // Call the function with an invalid pageToken const result = await queryCollectionGroup( 'testCollection', undefined, undefined, 20, 'invalid/doc/path' ); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid pagination token'); expect(result.content[0].text).toContain('Invalid document reference'); // Restore the original implementation vi.restoreAllMocks(); }); // Test for startAfter in queryCollectionGroup with valid pageToken (lines 437-439) it('should use startAfter with valid pageToken in queryCollectionGroup', async () => { // Mock admin.firestore().doc().get() to return a valid document const mockGet = vi.fn().mockResolvedValue({ exists: true }); const mockDoc = vi.fn().mockReturnValue({ get: mockGet, }); // Mock the query chain const mockStartAfter = vi.fn().mockReturnThis(); const mockLimit = vi.fn().mockReturnThis(); const mockGet2 = vi.fn().mockResolvedValue({ docs: [], }); const mockCollectionGroup = vi.fn().mockReturnValue({ startAfter: mockStartAfter, limit: mockLimit, get: mockGet2, }); vi.spyOn(admin.firestore(), 'doc').mockImplementation(mockDoc); vi.spyOn(admin.firestore(), 'collectionGroup').mockImplementation(mockCollectionGroup); // Call the function with a valid pageToken await queryCollectionGroup('testCollection', undefined, undefined, 20, 'valid/doc/path'); // Verify startAfter was called with the document expect(mockDoc).toHaveBeenCalledWith('valid/doc/path'); expect(mockGet).toHaveBeenCalled(); expect(mockStartAfter).toHaveBeenCalled(); // Restore the original implementation vi.restoreAllMocks(); }); // Test for JSON serialization errors in queryCollectionGroup (line 500) it('should handle JSON serialization errors in queryCollectionGroup', async () => { // Create a circular reference that will cause JSON.stringify to fail const circularObj: any = {}; circularObj.circular = circularObj; // Create circular reference // Mock the query result to include the circular reference const mockDocs = [ { id: 'doc1', ref: { path: 'test/doc1', id: 'doc1' }, data: () => ({ name: 'Test Document', circular: circularObj, // This will cause JSON.stringify to fail }), }, ]; // Mock the collection group query const mockGet = vi.fn().mockResolvedValue({ docs: mockDocs }); const mockCollectionGroup = vi.fn().mockReturnValue({ where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), startAfter: vi.fn().mockReturnThis(), get: mockGet, }); // Set up mocks vi.spyOn(admin.firestore(), 'collectionGroup').mockImplementation(mockCollectionGroup); // Set service account path to ensure code path is executed const originalPath = process.env.SERVICE_ACCOUNT_KEY_PATH; process.env.SERVICE_ACCOUNT_KEY_PATH = '/path/to/service-account.json'; // Mock getProjectId to return a valid project ID vi.spyOn(firebaseConfig, 'getProjectId').mockReturnValue('test-project'); try { // Call the function const result = await queryCollectionGroup('testCollection'); // Verify error response expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Converting circular structure to JSON'); } finally { // Restore original values process.env.SERVICE_ACCOUNT_KEY_PATH = originalPath; vi.restoreAllMocks(); } }); }); });

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/gannonh/firebase-mcp'

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