Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,17 @@ describe('DQL Explanation Integration Tests', () => {

expect(response.status === 'SUCCESSFUL' || response.status === 'SUCCESSFUL_WITH_WARNINGS').toBeTruthy();

// Can say any of:
// - "...calculate the total number of logs"
// - "...calculate the total number of log entries"
// - "...count the number of logs"
// - "...count the number of log entries"
//
// Can also fail intermittently with:
// - "I'm sorry, but I can't generate a response for you right now due to unusually high
// demand. Please try again in a few minutes."
expect(response.summary.toLowerCase()).toContain('group logs by');
expect(response.summary.toLowerCase()).toContain('calculate the total number of logs');
expect(response.summary.toLowerCase()).toMatch(/calculate the total number of log|count the number of log/);
// The explanation should be reasonably detailed
expect(response.explanation.length).toBeGreaterThan(50);
});
Expand Down
59 changes: 59 additions & 0 deletions integration-tests/dynatrace-clients.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Integration test for dynatrace client functionality
*
* Verifies authentication behaviour by making actual API calls
* to the Dynatrace environment.
*
* The error tests use deliberately incorrect authentication credentials.
*
* Other integration tests will use dynatrace-clients, so the happy-path is
* implicitly tested via other tests.
*/

import { config } from 'dotenv';
import { createDtHttpClient } from '../src/authentication/dynatrace-clients';
import { getDynatraceEnv, DynatraceEnv } from '../src/getDynatraceEnv';

// Load environment variables
config();

const API_RATE_LIMIT_DELAY = 100; // Delay in milliseconds to avoid hitting API rate limits

const scopesBase = [
'app-engine:apps:run', // needed for environmentInformationClient
'app-engine:functions:run', // needed for environmentInformationClient
];

describe('Dynatrace Clients Integration Tests', () => {
let dynatraceEnv: DynatraceEnv;

// Setup that runs once before all tests
beforeAll(async () => {
try {
dynatraceEnv = getDynatraceEnv();
console.log(`Testing against environment: ${dynatraceEnv.dtEnvironment}`);
} catch (err) {
throw new Error(`Environment configuration error: ${(err as Error).message}`);
}
});

afterEach(async () => {
// Add delay to avoid hitting API rate limits
await new Promise((resolve) => setTimeout(resolve, API_RATE_LIMIT_DELAY));
});

describe('Error Handling', () => {
it('should handle authentication errors gracefully', async () => {
// Create client with invalid credentials
await expect(
createDtHttpClient(
dynatraceEnv.dtEnvironment,
scopesBase,
'invalid-client-id',
'invalid-client-secret',
undefined,
),
).rejects.toThrow(`Failed to retrieve OAuth token`);
}, 30000);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

import { config } from 'dotenv';
import { createDtHttpClient } from '../src/authentication/dynatrace-clients';
import { findMonitoredEntityByName } from '../src/capabilities/find-monitored-entity-by-name';
import { findMonitoredEntitiesByName } from '../src/capabilities/find-monitored-entity-by-name';
import { getDynatraceEnv, DynatraceEnv } from '../src/getDynatraceEnv';
import { getEntityTypeFromId } from '../src/utils/dynatrace-entity-types';

// Load environment variables
config();
Expand Down Expand Up @@ -60,27 +61,25 @@ describe('Find Monitored Entity by Name Integration Tests', () => {

// Search for an entity name that is very unlikely to exist
const searchTerm = 'this-entity-definitely-does-not-exist-12345';
const extendedSearch = false;

const response = await findMonitoredEntityByName(dtClient, searchTerm);
const response = await findMonitoredEntitiesByName(dtClient, [searchTerm], extendedSearch);

expect(response).toBeDefined();
expect(typeof response).toBe('string');
expect(response).toBe('No monitored entity found with the specified name.');
expect(response?.records).toBeDefined();
expect(response?.records?.length).toEqual(0);
}, 30_000); // Increased timeout for API calls

test('should handle search with empty string', async () => {
test('should handle search with empty list', async () => {
const dtClient = await createHttpClient();

// Test with empty string
const searchTerm = '';
const searchTerms = [] as string[];
const extendedSearch = false;

const response = await findMonitoredEntityByName(dtClient, searchTerm);

expect(response).toBeDefined();
expect(typeof response).toBe('string');

// Should handle gracefully - likely will return many results or handle empty search
expect(response).toContain('You need to provide an entity name to search for');
await expect(findMonitoredEntitiesByName(dtClient, searchTerms, extendedSearch)).rejects.toThrow(
/No entity names supplied to search for/,
);
});

test('should return properly formatted response when entities are found', async () => {
Expand All @@ -89,20 +88,19 @@ describe('Find Monitored Entity by Name Integration Tests', () => {
// Search for a pattern that is likely to find at least one entity
// "host" is common in most Dynatrace environments
const searchTerm = 'host';
const extendedSearch = false;

const response = await findMonitoredEntityByName(dtClient, searchTerm);
const response = await findMonitoredEntitiesByName(dtClient, [searchTerm], extendedSearch);

// Assert, based on the DqlExecutionResult
expect(response).toBeDefined();
expect(typeof response).toBe('string');

// If entities are found, check the format
if (response.includes('The following monitored entities were found:')) {
// Each line should follow the expected format
const lines = response.split('\n').filter((line) => line.startsWith('- Entity'));

lines.forEach((line) => {
expect(line).toMatch(/^- Entity '.*' of type '.* has entity id '.*'$/);
if (response?.records && response.records.length > 0) {
response.records.forEach((entity) => {
expect(entity?.id).toBeDefined();
expect(getEntityTypeFromId(String(entity?.id))).toBeDefined();
});
} else {
// Nothing to assert; environment for testing has no entities found.
}
});
});
16 changes: 8 additions & 8 deletions integration-tests/send-email.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,25 +322,25 @@ All recipients should receive this message according to their designation.`,
});

describe('Error Handling', () => {
it('should handle authentication errors gracefully', async () => {
it('should handle authorization errors gracefully', async () => {
// Create client with invalid credentials
const dtClient = await createDtHttpClient(
dynatraceEnv.dtEnvironment,
scopesBase.concat(scopesEmail),
'invalid-client-id',
'invalid-client-secret',
undefined,
scopesBase.concat([]), // no scopesEmail
dynatraceEnv.oauthClientId,
dynatraceEnv.oauthClientSecret,
dynatraceEnv.dtPlatformToken,
);

const emailRequest: EmailRequest = {
toRecipients: { emailAddresses: [TEST_EMAIL_TO] },
subject: '[Integration Test] Authentication Error Test',
subject: '[Integration Test] Authorization Error Test',
body: {
body: 'This should fail due to invalid credentials.',
body: 'This should fail due to insufficient permissions.',
},
};

await expect(sendEmail(dtClient, emailRequest)).rejects.toThrow();
await expect(sendEmail(dtClient, emailRequest)).rejects.toThrow(`Forbidden`);
}, 30000);

it('should handle invalid email addresses appropriately', async () => {
Expand Down
4 changes: 4 additions & 0 deletions src/capabilities/find-monitored-entity-by-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
* @returns DQL Statement for searching all entity types
*/
export const generateDqlSearchEntityCommand = (entityNames: string[], extendedSearch: boolean): string => {
if (entityNames == undefined || entityNames.length == 0) {
throw new Error(`No entity names supplied to search for`);
}

// If extendedSearch is true, use all entity types, otherwise use only basic ones
const fetchDqlCommands = (extendedSearch ? DYNATRACE_ENTITY_TYPES_ALL : DYNATRACE_ENTITY_TYPES_BASICS).map(
(entityType, index) => {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ const main = async () => {
if (!result || result.length === 0) {
return 'No vulnerabilities found in the last 30 days';
}
let resp = `Found ${result.length} problems in the last 30 days! Displaying the top ${maxVulnerabilitiesToDisplay} problems:\n`;
let resp = `Found ${result.length} vulnerabilities in the last 30 days! Displaying the top ${maxVulnerabilitiesToDisplay} vulnerabilities:\n`;
result.slice(0, maxVulnerabilitiesToDisplay).forEach((vulnerability) => {
resp += `\n* ${vulnerability}`;
});
Expand Down