Skip to main content
Glama

Targetprocess MCP Server

tp.service.test.ts16.1 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import axios from 'axios'; import { TPService } from '../../api/client/tp.service.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { testConfig, getExpectedUrl } from '../config/test-config.js'; jest.mock('axios'); const mockedAxios = axios as jest.Mocked<typeof axios>; describe('TPService', () => { let service: TPService; beforeEach(() => { jest.clearAllMocks(); service = new TPService({ domain: testConfig.domain, credentials: { username: testConfig.username, password: testConfig.password } }); }); describe('constructor', () => { it('should initialize with basic auth credentials', () => { expect(service).toBeDefined(); expect((service as any).baseUrl).toBe(`https://${testConfig.domain}/api/v1`); }); it('should initialize with API key', () => { const apiKeyService = new TPService({ domain: testConfig.domain, apiKey: 'test-api-key' }); expect(apiKeyService).toBeDefined(); }); it('should throw error without credentials', () => { expect(() => new TPService({ domain: testConfig.domain } as any)).toThrow('Either credentials or apiKey must be provided'); }); }); describe('searchEntities', () => { it('should search with basic parameters', async () => { mockedAxios.get.mockResolvedValue({ data: { Items: [{ Id: 1, Name: 'Test' }], Next: null } }); const result = await service.searchEntities('UserStory', undefined, undefined, 10); expect(mockedAxios.get).toHaveBeenCalledWith( getExpectedUrl('/UserStory'), expect.objectContaining({ params: { take: 10, format: 'json' } }) ); expect(result).toHaveLength(1); }); it('should handle where clauses', async () => { mockedAxios.get.mockResolvedValue({ data: { Items: [], Next: null } }); await service.searchEntities('Bug', "Priority.Name = 'High'"); expect(mockedAxios.get).toHaveBeenCalledWith( getExpectedUrl('/Bug'), expect.objectContaining({ params: { where: "Priority.Name = 'High'", format: 'json' } }) ); }); it('should retry on 5xx errors', async () => { mockedAxios.get .mockRejectedValueOnce({ response: { status: 500 } }) .mockRejectedValueOnce({ response: { status: 503 } }) .mockResolvedValue({ data: { Items: [], Next: null } }); const result = await service.searchEntities('Task'); expect(mockedAxios.get).toHaveBeenCalledTimes(3); expect(result).toEqual([]); }); it('should not retry on 4xx errors', async () => { mockedAxios.get.mockRejectedValue({ response: { status: 400, data: { Message: 'Bad request' } } }); await expect(service.searchEntities('Project')) .rejects.toThrow(McpError); expect(mockedAxios.get).toHaveBeenCalledTimes(1); }); }); describe('getEntity', () => { it('should get entity by ID', async () => { const mockEntity = { Id: 123, Name: 'Test Entity' }; mockedAxios.get.mockResolvedValue({ data: mockEntity }); const result = await service.getEntity('UserStory', 123); expect(mockedAxios.get).toHaveBeenCalledWith( getExpectedUrl('/UserStory/123'), expect.objectContaining({ params: { format: 'json' } }) ); expect(result).toEqual(mockEntity); }); it('should include related entities', async () => { mockedAxios.get.mockResolvedValue({ data: { Id: 1, Project: { Id: 2, Name: 'Project' } } }); await service.getEntity('Bug', 1, ['Project', 'AssignedUser']); expect(mockedAxios.get).toHaveBeenCalledWith( getExpectedUrl('/Bug/1'), expect.objectContaining({ params: { include: 'Project,AssignedUser', format: 'json' } }) ); }); }); describe('createEntity', () => { it('should create entity with valid data', async () => { const newEntity = { Id: 456, Name: 'New Story' }; mockedAxios.post.mockResolvedValue({ data: newEntity }); const result = await service.createEntity('UserStory', { Name: 'New Story', Project: { Id: 1 } }); expect(mockedAxios.post).toHaveBeenCalledWith( getExpectedUrl('/UserStory'), { Name: 'New Story', Project: { Id: 1 } }, expect.objectContaining({ params: { format: 'json' } }) ); expect(result).toEqual(newEntity); }); }); describe('updateEntity', () => { it('should update entity fields', async () => { const updatedEntity = { Id: 789, Name: 'Updated' }; mockedAxios.post.mockResolvedValue({ data: updatedEntity }); const result = await service.updateEntity('Task', 789, { Name: 'Updated' }); expect(mockedAxios.post).toHaveBeenCalledWith( getExpectedUrl('/Task/789'), { Name: 'Updated' }, expect.objectContaining({ params: { format: 'json' } }) ); expect(result).toEqual(updatedEntity); }); }); describe('validateWhereClause', () => { it('should validate simple where clauses', () => { const testCases = [ "Name = 'Test'", "Id > 100", "Priority.Name != 'Low'", "CreateDate >= '2024-01-01'" ]; testCases.forEach(clause => { expect(() => (service as any).validateWhereClause(clause)) .not.toThrow(); }); }); it('should validate complex where clauses', () => { const clause = "(Project.Id = 1) and (State.Name = 'Open') or (Priority = 'High')"; expect(() => (service as any).validateWhereClause(clause)) .not.toThrow(); }); it('should reject invalid where clauses', () => { const invalidClauses = [ "Name = Test", // unquoted string "DROP TABLE Users", // SQL injection attempt "'; DELETE FROM", // injection attempt ]; invalidClauses.forEach(clause => { expect(() => (service as any).validateWhereClause(clause)) .toThrow(); }); }); }); // These tests were removed as validateEntityType is now a private method // The functionality is tested through the public API methods that use it describe('comment methods', () => { // Mock fetch globally for comment methods that use fetch instead of axios const mockFetch = jest.fn() as jest.MockedFunction<typeof globalThis.fetch>; // @ts-ignore - global fetch mock for tests (globalThis as any).fetch = mockFetch; beforeEach(() => { mockFetch.mockClear(); }); describe('getComments', () => { it('should get comments for an entity', async () => { const mockResponse = { Items: [ { Id: 207220, Description: 'Test comment', ParentId: null, CreateDate: '/Date(1752265210000+0200)/', IsPrivate: false, General: { Id: 54356, Name: 'Test Story' }, Owner: { Id: 101732, FullName: 'Test User', Login: 'test@example.com' } } ] }; mockFetch.mockResolvedValue({ ok: true, json: async () => mockResponse } as any); const result = await service.getComments('UserStory', 54356); expect(result).toEqual(mockResponse.Items); expect(mockFetch).toHaveBeenCalledWith( getExpectedUrl('/UserStory/54356/Comments'), expect.objectContaining({ headers: expect.objectContaining({ 'Authorization': expect.stringContaining('Basic') }) }) ); }); it('should handle invalid entity type', async () => { await expect(service.getComments('InvalidType', 54356)) .rejects .toThrow(McpError); }); it('should handle API errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', text: async () => 'Entity not found' } as any); await expect(service.getComments('UserStory', 99999)) .rejects .toThrow(); }); it('should retry on failure', async () => { // First call fails, second succeeds mockFetch .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', text: async () => 'Server error' } as any) .mockResolvedValueOnce({ ok: true, json: async () => ({ Items: [] }) } as any); const result = await service.getComments('Task', 12345); expect(result).toEqual([]); expect(mockFetch).toHaveBeenCalledTimes(2); }); }); describe('createComment', () => { it('should create a basic comment', async () => { const mockComment = { Id: 207221, Description: 'New comment', CreateDate: '/Date(1752265210000+0200)/', General: { Id: 54356 } }; mockFetch.mockResolvedValue({ ok: true, json: async () => mockComment } as any); const result = await service.createComment(54356, 'New comment'); expect(result).toEqual(mockComment); expect(mockFetch).toHaveBeenCalledWith( getExpectedUrl('/Comments'), expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/json', 'Authorization': expect.stringContaining('Basic') }), body: JSON.stringify({ General: { Id: 54356 }, Description: 'New comment' }) }) ); }); it('should create a private comment', async () => { const mockComment = { Id: 207222, Description: 'Private comment', IsPrivate: true, General: { Id: 54356 } }; mockFetch.mockResolvedValue({ ok: true, json: async () => mockComment } as any); await service.createComment(54356, 'Private comment', true); expect(mockFetch).toHaveBeenCalledWith( getExpectedUrl('/Comments'), expect.objectContaining({ body: JSON.stringify({ General: { Id: 54356 }, Description: 'Private comment', IsPrivate: true }) }) ); }); it('should create a reply comment', async () => { const mockReply = { Id: 207223, Description: 'Reply comment', ParentId: 207220, General: { Id: 54356 } }; mockFetch.mockResolvedValue({ ok: true, json: async () => mockReply } as any); await service.createComment(54356, 'Reply comment', false, 207220); expect(mockFetch).toHaveBeenCalledWith( getExpectedUrl('/Comments'), expect.objectContaining({ body: JSON.stringify({ General: { Id: 54356 }, Description: 'Reply comment', ParentId: 207220 }) }) ); }); it('should create a private reply comment', async () => { const mockReply = { Id: 207224, Description: 'Private reply', ParentId: 207220, IsPrivate: true, General: { Id: 54356 } }; mockFetch.mockResolvedValue({ ok: true, json: async () => mockReply } as any); await service.createComment(54356, 'Private reply', true, 207220); expect(mockFetch).toHaveBeenCalledWith( getExpectedUrl('/Comments'), expect.objectContaining({ body: JSON.stringify({ General: { Id: 54356 }, Description: 'Private reply', IsPrivate: true, ParentId: 207220 }) }) ); }); it('should handle API errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 400, statusText: 'Bad Request', text: async () => 'Invalid comment data' } as any); await expect(service.createComment(54356, 'Test comment')) .rejects .toThrow(); }); it('should retry on failure', async () => { const mockComment = { Id: 207225, Description: 'Retry comment' }; mockFetch .mockResolvedValueOnce({ ok: false, status: 503, statusText: 'Service Unavailable', text: async () => 'Service temporarily unavailable' } as any) .mockResolvedValueOnce({ ok: true, json: async () => mockComment } as any); const result = await service.createComment(54356, 'Retry comment'); expect(result).toEqual(mockComment); expect(mockFetch).toHaveBeenCalledTimes(2); }); }); describe('deleteComment', () => { it('should delete a comment successfully', async () => { mockFetch.mockResolvedValue({ ok: true, status: 200 } as any); const result = await service.deleteComment(207220); expect(result).toBe(true); expect(mockFetch).toHaveBeenCalledWith( getExpectedUrl('/Comments/207220'), expect.objectContaining({ method: 'DELETE', headers: expect.objectContaining({ 'Authorization': expect.stringContaining('Basic') }) }) ); }); it('should handle delete failures', async () => { mockFetch.mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', text: async () => 'Comment not found' } as any); await expect(service.deleteComment(999999)) .rejects .toThrow('Failed to delete comment 999999: 404 - Comment not found'); }); it('should handle unauthorized deletion', async () => { mockFetch.mockResolvedValue({ ok: false, status: 403, statusText: 'Forbidden', text: async () => 'Insufficient permissions' } as any); await expect(service.deleteComment(207220)) .rejects .toThrow('Failed to delete comment 207220: 403 - Insufficient permissions'); }); it('should retry on transient failures', async () => { mockFetch .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', text: async () => 'Temporary server error' } as any) .mockResolvedValueOnce({ ok: true, status: 200 } as any); const result = await service.deleteComment(207220); expect(result).toBe(true); expect(mockFetch).toHaveBeenCalledTimes(2); }); it('should not retry on 4xx errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 400, statusText: 'Bad Request', text: async () => 'Invalid comment ID' } as any); await expect(service.deleteComment(-1)) .rejects .toThrow(); expect(mockFetch).toHaveBeenCalledTimes(1); // No retry on 4xx }); it('should handle network failures', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(service.deleteComment(207220)) .rejects .toThrow(); }); }); }); });

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/aaronsb/apptio-target-process-mcp'

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