Skip to main content
Glama
by thoughtspot
thoughtspot-client.spec.ts22.8 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { getThoughtSpotClient } from '../../src/thoughtspot/thoughtspot-client'; import { createBearerAuthenticationConfig, ThoughtSpotRestApi } from '@thoughtspot/rest-api-sdk'; import type { RequestContext, ResponseContext } from '@thoughtspot/rest-api-sdk'; import { of } from 'rxjs'; import YAML from 'yaml'; // Mock the ThoughtSpot REST API SDK vi.mock('@thoughtspot/rest-api-sdk', () => ({ createBearerAuthenticationConfig: vi.fn(), ThoughtSpotRestApi: vi.fn(), })); // Mock fetch global.fetch = vi.fn(); // Mock YAML vi.mock('yaml', () => ({ default: { parse: vi.fn(), }, })); describe('ThoughtSpot Client', () => { const mockInstanceUrl = 'https://test.thoughtspot.com'; const mockBearerToken = 'test-token-123'; let mockConfig: any; let mockClient: any; beforeEach(() => { vi.clearAllMocks(); // Setup mock config mockConfig = { middleware: [], }; // Setup mock client mockClient = { instanceUrl: mockInstanceUrl, }; (createBearerAuthenticationConfig as any).mockReturnValue(mockConfig); (ThoughtSpotRestApi as any).mockImplementation(() => mockClient); }); afterEach(() => { vi.restoreAllMocks(); }); describe('getThoughtSpotClient', () => { it('should create a ThoughtSpot client with bearer authentication', () => { const client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken) as any; expect(createBearerAuthenticationConfig).toHaveBeenCalledWith( mockInstanceUrl, expect.any(Function) ); expect(ThoughtSpotRestApi).toHaveBeenCalledWith(mockConfig); expect(client).toBe(mockClient); expect(client.instanceUrl).toBe(mockInstanceUrl); }); it('should add middleware with Accept-Language header', async () => { const client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken); expect(mockConfig.middleware).toHaveLength(1); const middleware = mockConfig.middleware[0]; expect(middleware).toHaveProperty('pre'); expect(middleware).toHaveProperty('post'); // Test pre middleware const mockContext = { getHeaders: vi.fn().mockReturnValue({}), setHeaderParam: vi.fn(), }; const preResult = await middleware.pre(mockContext).toPromise(); expect(mockContext.getHeaders).toHaveBeenCalled(); expect(mockContext.setHeaderParam).toHaveBeenCalledWith('Accept-Language', 'en-US'); expect(preResult).toBe(mockContext); }); it('should not override existing Accept-Language header', async () => { const client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken); const middleware = mockConfig.middleware[0]; const mockContext = { getHeaders: vi.fn().mockReturnValue({ 'Accept-Language': 'fr-FR' }), setHeaderParam: vi.fn(), }; await middleware.pre(mockContext).toPromise(); expect(mockContext.setHeaderParam).not.toHaveBeenCalled(); }); it('should handle post middleware correctly', async () => { const client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken); const middleware = mockConfig.middleware[0]; const mockContext = {} as ResponseContext; const postResult = await middleware.post(mockContext).toPromise(); expect(postResult).toBe(mockContext); }); it('should add custom methods to the client', () => { const client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken) as any; expect(client).toHaveProperty('exportUnsavedAnswerTML'); expect(client).toHaveProperty('getSessionInfo'); expect(client).toHaveProperty('queryGetDataSourceSuggestions'); expect(typeof client.exportUnsavedAnswerTML).toBe('function'); expect(typeof client.getSessionInfo).toBe('function'); expect(typeof client.queryGetDataSourceSuggestions).toBe('function'); }); }); describe('exportUnsavedAnswerTML', () => { let client: any; beforeEach(() => { client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken) as any; }); it('should export unsaved answer TML successfully', async () => { const mockResponse = { data: { UnsavedAnswer_getTML: { object: [{ edoc: 'test-yaml-content' }] } } }; const mockYamlParsed = { test: 'data' }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); (YAML.parse as any).mockReturnValue(mockYamlParsed); const result = await client.exportUnsavedAnswerTML({ session_identifier: 'session-123', generation_number: 1 }); expect(fetch).toHaveBeenCalledWith(`${mockInstanceUrl}/prism/?op=GetUnsavedAnswerTML`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'user-agent': 'ThoughtSpot-ts-client', 'Authorization': 'Bearer test-token-123', }, body: expect.any(String) }); // Verify the body contains expected data const fetchCall = (fetch as any).mock.calls[0]; const body = JSON.parse(fetchCall[1].body); expect(body.operationName).toBe('GetUnsavedAnswerTML'); expect(body.variables.session.sessionId).toBe('session-123'); expect(body.variables.session.genNo).toBe(1); expect(YAML.parse).toHaveBeenCalledWith('test-yaml-content'); expect(result).toEqual(mockYamlParsed); }); it('should handle fetch errors', async () => { const mockError = new Error('Network error'); (fetch as any).mockRejectedValue(mockError); await expect(client.exportUnsavedAnswerTML({ session_identifier: 'session-123', generation_number: 1 })).rejects.toThrow('Network error'); }); it('should handle malformed response data', async () => { const mockResponse = { data: { UnsavedAnswer_getTML: { object: [] // Empty array } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); await expect(client.exportUnsavedAnswerTML({ session_identifier: 'session-123', generation_number: 1 })).rejects.toThrow(); }); }); describe('getSessionInfo', () => { let client: any; beforeEach(() => { client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken) as any; }); it('should get session info successfully', async () => { const mockResponse = { info: { userId: 'user-123', userName: 'test-user', email: 'test@example.com', displayName: 'Test User', tenantId: 'tenant-123', locale: 'en-US', timezone: 'UTC' } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.getSessionInfo(); expect(fetch).toHaveBeenCalledWith(`${mockInstanceUrl}/prism/preauth/info`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'user-agent': 'ThoughtSpot-ts-client', 'Authorization': `Bearer ${mockBearerToken}`, } }); expect(result).toEqual(mockResponse.info); }); it('should handle fetch errors', async () => { const mockError = new Error('Network error'); (fetch as any).mockRejectedValue(mockError); await expect(client.getSessionInfo()).rejects.toThrow('Network error'); }); it('should handle HTTP error responses', async () => { const mockResponse = { ok: false, status: 401, statusText: 'Unauthorized', json: vi.fn().mockResolvedValue({ error: 'Invalid token' }) }; (fetch as any).mockResolvedValue(mockResponse); // The actual implementation doesn't check response.ok, so it will try to parse the response const result = await client.getSessionInfo(); expect(result).toBeUndefined(); // data.info will be undefined }); it('should handle malformed response', async () => { const mockResponse = { // Missing info property someOtherProperty: 'value' }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.getSessionInfo(); expect(result).toBeUndefined(); }); it('should handle empty response', async () => { const mockResponse = {}; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.getSessionInfo(); expect(result).toBeUndefined(); }); it('should handle null response', async () => { const mockResponse = null; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); // The actual implementation will throw when trying to access data.info on null await expect(client.getSessionInfo()).rejects.toThrow(); }); it('should handle partial session info', async () => { const mockResponse = { info: { userId: 'user-123', userName: 'test-user' // Missing other properties } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.getSessionInfo(); expect(result).toEqual(mockResponse.info); expect(result.userId).toBe('user-123'); expect(result.userName).toBe('test-user'); expect(result.email).toBeUndefined(); }); it('should use correct headers for session info request', async () => { const mockResponse = { info: { userId: 'user-123', userName: 'test-user' } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); await client.getSessionInfo(); const fetchCall = (fetch as any).mock.calls[0]; const headers = fetchCall[1].headers; expect(headers['Content-Type']).toBe('application/json'); expect(headers.Accept).toBe('application/json'); expect(headers['user-agent']).toBe('ThoughtSpot-ts-client'); expect(headers.Authorization).toBe(`Bearer ${mockBearerToken}`); }); it('should handle JSON parsing errors', async () => { (fetch as any).mockResolvedValue({ json: vi.fn().mockRejectedValue(new Error('Invalid JSON')) }); await expect(client.getSessionInfo()).rejects.toThrow('Invalid JSON'); }); }); describe('queryGetDataSourceSuggestions', () => { let client: any; beforeEach(() => { client = getThoughtSpotClient(mockInstanceUrl, mockBearerToken) as any; }); it('should query data source suggestions successfully', async () => { const mockResponse = { data: { queryGetDataSourceSuggestions: { dataSources: [ { confidence: 0.95, header: { description: 'Sales data with customer information', displayName: 'Sales Database', guid: 'sales-db-guid-123' }, llmReasoning: 'This datasource contains sales information that matches your query about revenue' }, { confidence: 0.78, header: { description: 'Customer relationship management data', displayName: 'CRM Database', guid: 'crm-db-guid-456' }, llmReasoning: 'This datasource has customer data that could be relevant to your analysis' } ] } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.queryGetDataSourceSuggestions('Show me sales revenue by customer'); expect(fetch).toHaveBeenCalledWith(`${mockInstanceUrl}/prism/`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'user-agent': 'ThoughtSpot-ts-client', 'Authorization': `Bearer ${mockBearerToken}`, }, body: expect.any(String) }); // Verify the body contains expected GraphQL query data const fetchCall = (fetch as any).mock.calls[0]; const body = JSON.parse(fetchCall[1].body); expect(body.operationName).toBe('QueryGetDataSourceSuggestions'); expect(body.variables.request.query).toBe('Show me sales revenue by customer'); expect(body.query).toContain('queryGetDataSourceSuggestions'); expect(body.query).toContain('dataSources'); expect(body.query).toContain('confidence'); expect(body.query).toContain('header'); expect(body.query).toContain('llmReasoning'); expect(result).toEqual(mockResponse.data.queryGetDataSourceSuggestions); expect(result.dataSources).toHaveLength(2); expect(result.dataSources[0].confidence).toBe(0.95); expect(result.dataSources[0].header.displayName).toBe('Sales Database'); }); it('should handle empty data source suggestions response', async () => { const mockResponse = { data: { queryGetDataSourceSuggestions: { dataSources: [] } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.queryGetDataSourceSuggestions('no matching data'); expect(result.dataSources).toEqual([]); expect(result.dataSources).toHaveLength(0); }); it('should handle network errors', async () => { const mockError = new Error('Network connection failed'); (fetch as any).mockRejectedValue(mockError); await expect(client.queryGetDataSourceSuggestions('test query')).rejects.toThrow('Network connection failed'); }); it('should handle HTTP error responses', async () => { const mockResponse = { ok: false, status: 500, statusText: 'Internal Server Error', json: vi.fn().mockResolvedValue({ error: 'Server error' }) }; (fetch as any).mockResolvedValue(mockResponse); // The actual implementation doesn't check response.ok, so it will try to parse the response // Since the response doesn't have a 'data' property, accessing data.data will throw await expect(client.queryGetDataSourceSuggestions('test query')).rejects.toThrow(); }); it('should handle malformed GraphQL response', async () => { const mockResponse = { data: { // Missing queryGetDataSourceSuggestions property someOtherProperty: 'value' } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.queryGetDataSourceSuggestions('test query'); expect(result).toBeUndefined(); }); it('should handle GraphQL errors in response', async () => { const mockResponse = { errors: [ { message: 'Authentication failed', locations: [{ line: 2, column: 3 }], path: ['queryGetDataSourceSuggestions'] } ] }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); // The actual implementation will throw when trying to access data.data on undefined (no data property when there are errors) await expect(client.queryGetDataSourceSuggestions('test query')).rejects.toThrow(); }); it('should handle null response data', async () => { const mockResponse = null; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); // The actual implementation will throw when trying to access data.queryGetDataSourceSuggestions on null await expect(client.queryGetDataSourceSuggestions('test query')).rejects.toThrow(); }); it('should handle JSON parsing errors', async () => { (fetch as any).mockResolvedValue({ json: vi.fn().mockRejectedValue(new Error('Invalid JSON response')) }); await expect(client.queryGetDataSourceSuggestions('test query')).rejects.toThrow('Invalid JSON response'); }); it('should use correct request headers and format', async () => { const mockResponse = { data: { queryGetDataSourceSuggestions: { dataSources: [] } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); await client.queryGetDataSourceSuggestions('sales data analysis'); const fetchCall = (fetch as any).mock.calls[0]; const url = fetchCall[0]; const options = fetchCall[1]; expect(url).toBe(`${mockInstanceUrl}/prism/`); expect(options.method).toBe('POST'); expect(options.headers['Content-Type']).toBe('application/json'); expect(options.headers.Accept).toBe('application/json'); expect(options.headers['user-agent']).toBe('ThoughtSpot-ts-client'); expect(options.headers.Authorization).toBe(`Bearer ${mockBearerToken}`); const body = JSON.parse(options.body); expect(body.operationName).toBe('QueryGetDataSourceSuggestions'); expect(body.variables.request.query).toBe('sales data analysis'); }); it('should handle single data source suggestion', async () => { const mockResponse = { data: { queryGetDataSourceSuggestions: { dataSources: [ { confidence: 0.88, header: { description: 'Financial data warehouse', displayName: 'Finance DW', guid: 'finance-dw-guid-789' }, llmReasoning: 'Contains comprehensive financial metrics and KPIs' } ] } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.queryGetDataSourceSuggestions('financial metrics'); expect(result.dataSources).toHaveLength(1); expect(result.dataSources[0].confidence).toBe(0.88); expect(result.dataSources[0].header.displayName).toBe('Finance DW'); expect(result.dataSources[0].header.guid).toBe('finance-dw-guid-789'); expect(result.dataSources[0].llmReasoning).toBe('Contains comprehensive financial metrics and KPIs'); }); it('should handle partial data source suggestion data', async () => { const mockResponse = { data: { queryGetDataSourceSuggestions: { dataSources: [ { confidence: 0.75, header: { displayName: 'Incomplete Data Source', guid: 'incomplete-guid-999' // Missing description }, llmReasoning: 'This has partial data' } ] } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); const result = await client.queryGetDataSourceSuggestions('test query'); expect(result.dataSources).toHaveLength(1); expect(result.dataSources[0].confidence).toBe(0.75); expect(result.dataSources[0].header.displayName).toBe('Incomplete Data Source'); expect(result.dataSources[0].header.description).toBeUndefined(); }); it('should handle different query types', async () => { const mockResponse = { data: { queryGetDataSourceSuggestions: { dataSources: [] } } }; (fetch as any).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }); // Test with empty string await client.queryGetDataSourceSuggestions(''); expect((fetch as any).mock.calls[0][1].body).toContain('"query":""'); // Test with special characters await client.queryGetDataSourceSuggestions('query with "quotes" and %symbols%'); expect((fetch as any).mock.calls[1][1].body).toContain('query with \\"quotes\\" and %symbols%'); // Test with very long query const longQuery = 'a'.repeat(1000); await client.queryGetDataSourceSuggestions(longQuery); expect((fetch as any).mock.calls[2][1].body).toContain(longQuery); }); }); describe('GraphQL Queries', () => { it('should have the correct GraphQL mutation structure for GetUnsavedAnswerTML', () => { // This test ensures the GraphQL query is properly structured const query = ` mutation GetUnsavedAnswerTML($session: BachSessionIdInput!, $exportDependencies: Boolean, $formatType: EDocFormatType, $exportPermissions: Boolean, $exportFqn: Boolean) { UnsavedAnswer_getTML( session: $session exportDependencies: $exportDependencies formatType: $formatType exportPermissions: $exportPermissions exportFqn: $exportFqn ) { zipFile object { edoc name type __typename } __typename } }`; expect(query).toContain('mutation GetUnsavedAnswerTML'); expect(query).toContain('BachSessionIdInput'); expect(query).toContain('UnsavedAnswer_getTML'); expect(query).toContain('edoc'); }); it('should have the correct GraphQL query structure for QueryGetDataSourceSuggestions', () => { // This test ensures the data source suggestions GraphQL query is properly structured const query = ` query QueryGetDataSourceSuggestions($request: Input_eureka_DataSourceSuggestionRequest) { queryGetDataSourceSuggestions(request: $request) { dataSources { confidence header { description displayName guid } llmReasoning } } }`; expect(query).toContain('query QueryGetDataSourceSuggestions'); expect(query).toContain('Input_eureka_DataSourceSuggestionRequest'); expect(query).toContain('queryGetDataSourceSuggestions'); expect(query).toContain('dataSources'); expect(query).toContain('confidence'); expect(query).toContain('header'); expect(query).toContain('description'); expect(query).toContain('displayName'); expect(query).toContain('guid'); expect(query).toContain('llmReasoning'); }); }); });

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/thoughtspot/mcp-server'

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