Skip to main content
Glama

Dynatrace MCP Server

Official
dynatrace-clients.test.ts8.8 kB
import { createDtHttpClient } from './dynatrace-clients'; import { PlatformHttpClient } from '@dynatrace-sdk/http-client'; import { getSSOUrl } from 'dt-app'; import { OAuthTokenResponse } from './types'; import { performOAuthAuthorizationCodeFlow } from './dynatrace-oauth-auth-code-flow'; import { globalTokenCache } from './token-cache'; // Mock external dependencies jest.mock('@dynatrace-sdk/http-client'); jest.mock('dt-app'); jest.mock('./dynatrace-oauth-auth-code-flow'); jest.mock('./token-cache'); jest.mock('../../package.json', () => ({ version: '1.0.0-test', })); // Mock fetch globally global.fetch = jest.fn(); const mockPlatformHttpClient = PlatformHttpClient as jest.MockedClass<typeof PlatformHttpClient>; const mockGetSSOUrl = getSSOUrl as jest.MockedFunction<typeof getSSOUrl>; const mockPerformOAuthAuthorizationCodeFlow = performOAuthAuthorizationCodeFlow as jest.MockedFunction< typeof performOAuthAuthorizationCodeFlow >; const mockGlobalTokenCache = globalTokenCache as jest.Mocked<typeof globalTokenCache>; const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>; describe('dynatrace-clients', () => { beforeEach(() => { jest.clearAllMocks(); // Reset console.error mock jest.spyOn(console, 'error').mockImplementation(() => {}); // Mock token cache methods mockGlobalTokenCache.getToken.mockReturnValue(null); mockGlobalTokenCache.isTokenValid.mockReturnValue(false); mockGlobalTokenCache.setToken.mockImplementation(() => {}); mockGlobalTokenCache.clearToken.mockImplementation(() => {}); // Mock getSSOUrl mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com'); }); afterEach(() => { jest.restoreAllMocks(); }); describe('createDtHttpClient', () => { const environmentUrl = 'https://test123.apps.dynatrace.com'; const scopes = ['scope1', 'scope2']; describe('with OAuth credentials', () => { const clientId = 'test-client-id'; const clientSecret = 'test-client-secret'; const platformToken = 'test-platform-token'; beforeEach(() => { mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com'); }); it('should create OAuth client successfully', async () => { const mockTokenResponse: OAuthTokenResponse = { access_token: 'test-access-token', token_type: 'Bearer', expires_in: 3600, scope: 'scope1 scope2', }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockTokenResponse, } as Response); const result = await createDtHttpClient(environmentUrl, scopes, clientId, clientSecret); expect(mockGetSSOUrl).toHaveBeenCalledWith(environmentUrl); expect(mockFetch).toHaveBeenCalledWith('https://sso.dynatrace.com/sso/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret, scope: scopes.join(' '), }), }); expect(mockPlatformHttpClient).toHaveBeenCalledWith({ baseUrl: environmentUrl, defaultHeaders: { 'Authorization': 'Bearer test-access-token', 'User-Agent': expect.stringMatching(/^dynatrace-mcp-server\/v1\.0\.0-test \(\w+-\w+\)$/), }, }); expect(result).toBeInstanceOf(PlatformHttpClient); }); it('should throw error when token request fails with HTTP error', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', json: async () => ({ error: 'invalid_client', error_description: 'Invalid client credentials', }), } as Response); await expect(createDtHttpClient(environmentUrl, scopes, clientId, clientSecret)).rejects.toThrow( 'Failed to retrieve OAuth token', ); expect(console.error).toHaveBeenCalledWith('Failed to fetch token: 401 Unauthorized'); }); it('should throw error when token response contains error', async () => { const mockErrorResponse: OAuthTokenResponse = { error: 'invalid_scope', error_description: 'The requested scope is invalid', issueId: 'issue-123', }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockErrorResponse, } as Response); await expect(createDtHttpClient(environmentUrl, scopes, clientId, clientSecret)).rejects.toThrow( 'Failed to retrieve OAuth token (IssueId: issue-123): invalid_scope - The requested scope is invalid', ); }); it('should throw error when token response is missing access_token', async () => { const mockIncompleteResponse: OAuthTokenResponse = { token_type: 'Bearer', expires_in: 3600, }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockIncompleteResponse, } as Response); await expect(createDtHttpClient(environmentUrl, scopes, clientId, clientSecret)).rejects.toThrow( 'Failed to retrieve OAuth token', ); }); it('should log authentication details', async () => { const mockTokenResponse: OAuthTokenResponse = { access_token: 'test-access-token', token_type: 'Bearer', expires_in: 3600, }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockTokenResponse, } as Response); await createDtHttpClient(environmentUrl, scopes, clientId, clientSecret); expect(console.error).toHaveBeenCalledWith( `🔒 Client-Creds-Flow: Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`, ); }); }); describe('with Bearer token', () => { const dtPlatformToken = 'test-platform-token'; it('should create Bearer token client successfully', async () => { const result = await createDtHttpClient(environmentUrl, scopes, undefined, undefined, dtPlatformToken); expect(mockPlatformHttpClient).toHaveBeenCalledWith({ baseUrl: environmentUrl, defaultHeaders: { 'Authorization': `Bearer ${dtPlatformToken}`, 'User-Agent': expect.stringMatching(/^dynatrace-mcp-server\/v1\.0\.0-test \(\w+-\w+\)$/), }, }); expect(result).toBeInstanceOf(PlatformHttpClient); }); }); }); describe('requestToken function (indirectly tested)', () => { it('should handle fetch errors gracefully', async () => { mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com'); // Mock fetch to throw an error mockFetch.mockRejectedValueOnce(new Error('Network error')); await expect( createDtHttpClient('https://test.apps.dynatrace.com', ['scope1'], 'client-id', 'client-secret'), ).rejects.toThrow('Network error'); }); it('should format request body correctly', async () => { mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com'); const mockTokenResponse: OAuthTokenResponse = { access_token: 'test-token', }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockTokenResponse, } as Response); await createDtHttpClient('https://test.apps.dynatrace.com', ['scope1', 'scope2'], 'test-client', 'test-secret'); const expectedBody = new URLSearchParams({ grant_type: 'client_credentials', client_id: 'test-client', client_secret: 'test-secret', scope: 'scope1 scope2', }); expect(mockFetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: expectedBody, }), ); }); }); describe('User-Agent header', () => { it('should include correct User-Agent format', async () => { const dtPlatformToken = 'test-token'; await createDtHttpClient('https://test.apps.dynatrace.com', ['scope1'], undefined, undefined, dtPlatformToken); expect(mockPlatformHttpClient).toHaveBeenCalledWith( expect.objectContaining({ defaultHeaders: expect.objectContaining({ 'User-Agent': expect.stringMatching(/^dynatrace-mcp-server\/v\d+\.\d+\.\d+(-\w+)? \(\w+-\w+\)$/), }), }), ); }); }); });

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/dynatrace-oss/dynatrace-mcp'

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