|
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | 4 | import { describe, expect, it } from 'vitest' |
| 5 | +import { closeProblemTool } from '@/tools/dynatrace/close_problem' |
5 | 6 | import { getAuditLogsTool } from '@/tools/dynatrace/get_audit_logs' |
6 | 7 | import { getEntityTool } from '@/tools/dynatrace/get_entity' |
7 | 8 | import { getMetricTool } from '@/tools/dynatrace/get_metric' |
8 | 9 | import { getProblemTool } from '@/tools/dynatrace/get_problem' |
| 10 | +import { getSloTool } from '@/tools/dynatrace/get_slo' |
9 | 11 | import { ingestEventTool } from '@/tools/dynatrace/ingest_event' |
10 | 12 | import { ingestLogsTool } from '@/tools/dynatrace/ingest_logs' |
| 13 | +import { ingestMetricsTool } from '@/tools/dynatrace/ingest_metrics' |
| 14 | +import { listEntitiesTool } from '@/tools/dynatrace/list_entities' |
| 15 | +import { listEntityTypesTool } from '@/tools/dynatrace/list_entity_types' |
| 16 | +import { listEventsTool } from '@/tools/dynatrace/list_events' |
| 17 | +import { listMetricsTool } from '@/tools/dynatrace/list_metrics' |
| 18 | +import { listProblemCommentsTool } from '@/tools/dynatrace/list_problem_comments' |
11 | 19 | import { listProblemsTool } from '@/tools/dynatrace/list_problems' |
| 20 | +import { listSecurityProblemsTool } from '@/tools/dynatrace/list_security_problems' |
| 21 | +import { listSlosTool } from '@/tools/dynatrace/list_slos' |
| 22 | +import { queryMetricsTool } from '@/tools/dynatrace/query_metrics' |
| 23 | +import { searchLogsTool } from '@/tools/dynatrace/search_logs' |
12 | 24 | import { buildDynatraceUrl, dynatraceHeaders } from '@/tools/dynatrace/utils' |
13 | 25 | import { ErrorExtractorId, extractErrorMessageWithId } from '@/tools/error-extractors' |
14 | 26 |
|
@@ -246,6 +258,160 @@ describe('response mapping', () => { |
246 | 258 | expect(result.output).toEqual({ accepted: true, statusCode: 204, details: null }) |
247 | 259 | }) |
248 | 260 |
|
| 261 | + /** |
| 262 | + * A wrong top-level key does not throw — it yields an empty list and looks like |
| 263 | + * "no results". Each payload below is shaped exactly like the documented schema, |
| 264 | + * so an incorrect key fails loudly here instead of silently in production. |
| 265 | + */ |
| 266 | + it('reads the documented top-level key of every list response', async () => { |
| 267 | + const cases: Array<{ |
| 268 | + name: string |
| 269 | + tool: { transformResponse?: (r: Response) => Promise<{ output: Record<string, never> }> } |
| 270 | + payload: Record<string, unknown> |
| 271 | + read: (out: Record<string, never>) => unknown |
| 272 | + }> = [ |
| 273 | + { |
| 274 | + name: 'GET /slo -> slo', |
| 275 | + tool: listSlosTool, |
| 276 | + payload: { totalCount: 1, slo: [{ id: 'SLO-1', name: 'Checkout', status: 'WARNING' }] }, |
| 277 | + read: (o) => o.slos, |
| 278 | + }, |
| 279 | + { |
| 280 | + name: 'GET /metrics/query -> result', |
| 281 | + tool: queryMetricsTool, |
| 282 | + payload: { |
| 283 | + resolution: '1h', |
| 284 | + result: [ |
| 285 | + { |
| 286 | + metricId: 'builtin:host.cpu.usage', |
| 287 | + data: [{ dimensions: ['HOST-1'], timestamps: [1], values: [42.5] }], |
| 288 | + }, |
| 289 | + ], |
| 290 | + }, |
| 291 | + read: (o) => o.result, |
| 292 | + }, |
| 293 | + { |
| 294 | + name: 'GET /metrics -> metrics', |
| 295 | + tool: listMetricsTool, |
| 296 | + payload: { totalCount: 1, metrics: [{ metricId: 'builtin:host.cpu.usage' }] }, |
| 297 | + read: (o) => o.metrics, |
| 298 | + }, |
| 299 | + { |
| 300 | + name: 'GET /entities -> entities', |
| 301 | + tool: listEntitiesTool, |
| 302 | + payload: { totalCount: 1, entities: [{ entityId: 'HOST-1', type: 'HOST' }] }, |
| 303 | + read: (o) => o.entities, |
| 304 | + }, |
| 305 | + { |
| 306 | + name: 'GET /entityTypes -> types', |
| 307 | + tool: listEntityTypesTool, |
| 308 | + payload: { totalCount: 1, types: [{ type: 'HOST', displayName: 'Host' }] }, |
| 309 | + read: (o) => o.types, |
| 310 | + }, |
| 311 | + { |
| 312 | + name: 'GET /events -> events', |
| 313 | + tool: listEventsTool, |
| 314 | + payload: { totalCount: 1, events: [{ eventId: 'E-1', eventType: 'CUSTOM_DEPLOYMENT' }] }, |
| 315 | + read: (o) => o.events, |
| 316 | + }, |
| 317 | + { |
| 318 | + name: 'GET /securityProblems -> securityProblems', |
| 319 | + tool: listSecurityProblemsTool, |
| 320 | + payload: { |
| 321 | + totalCount: 1, |
| 322 | + securityProblems: [{ securityProblemId: 'S-1', status: 'OPEN' }], |
| 323 | + }, |
| 324 | + read: (o) => o.securityProblems, |
| 325 | + }, |
| 326 | + { |
| 327 | + name: 'GET /logs/search -> results', |
| 328 | + tool: searchLogsTool, |
| 329 | + payload: { sliceSize: 1, results: [{ timestamp: 1, status: 'ERROR', content: 'boom' }] }, |
| 330 | + read: (o) => o.results, |
| 331 | + }, |
| 332 | + { |
| 333 | + name: 'GET /problems/{id}/comments -> comments', |
| 334 | + tool: listProblemCommentsTool, |
| 335 | + payload: { totalCount: 1, comments: [{ id: 'C-1', content: 'looking into it' }] }, |
| 336 | + read: (o) => o.comments, |
| 337 | + }, |
| 338 | + { |
| 339 | + name: 'GET /auditlogs -> auditLogs', |
| 340 | + tool: getAuditLogsTool, |
| 341 | + payload: { totalCount: 1, auditLogs: [{ logId: 'L-1' }] }, |
| 342 | + read: (o) => o.auditLogs, |
| 343 | + }, |
| 344 | + ] |
| 345 | + |
| 346 | + for (const { name, tool, payload, read } of cases) { |
| 347 | + const out = ( |
| 348 | + await tool.transformResponse!(new Response(JSON.stringify(payload), { status: 200 })) |
| 349 | + ).output |
| 350 | + expect(read(out), `${name} produced an empty list`).toHaveLength(1) |
| 351 | + } |
| 352 | + }) |
| 353 | + |
| 354 | + it('reads the documented scalar keys of the ingest and single-entity responses', async () => { |
| 355 | + const metrics = ( |
| 356 | + await ingestMetricsTool.transformResponse!( |
| 357 | + new Response(JSON.stringify({ linesOk: 7, linesInvalid: 1, error: { code: 400 } }), { |
| 358 | + status: 202, |
| 359 | + }) |
| 360 | + ) |
| 361 | + ).output |
| 362 | + expect(metrics.linesOk).toBe(7) |
| 363 | + expect(metrics.linesInvalid).toBe(1) |
| 364 | + expect(metrics.ingestError).toEqual({ code: 400 }) |
| 365 | + |
| 366 | + const event = ( |
| 367 | + await ingestEventTool.transformResponse!( |
| 368 | + new Response( |
| 369 | + JSON.stringify({ |
| 370 | + reportCount: 1, |
| 371 | + eventIngestResults: [{ correlationId: 'c-1', status: 'OK' }], |
| 372 | + }), |
| 373 | + { status: 201 } |
| 374 | + ) |
| 375 | + ) |
| 376 | + ).output |
| 377 | + expect(event.reportCount).toBe(1) |
| 378 | + expect(event.eventIngestResults).toEqual([{ correlationId: 'c-1', status: 'OK' }]) |
| 379 | + |
| 380 | + // Single-entity endpoints return the object at the document root, not nested. |
| 381 | + const slo = ( |
| 382 | + await getSloTool.transformResponse!( |
| 383 | + new Response(JSON.stringify({ id: 'SLO-1', name: 'Checkout', evaluatedPercentage: 99.5 }), { |
| 384 | + status: 200, |
| 385 | + }) |
| 386 | + ) |
| 387 | + ).output |
| 388 | + expect(slo.slo.id).toBe('SLO-1') |
| 389 | + expect(slo.slo.evaluatedPercentage).toBe(99.5) |
| 390 | + |
| 391 | + const entity = ( |
| 392 | + await getEntityTool.transformResponse!( |
| 393 | + new Response(JSON.stringify({ entityId: 'HOST-1', displayName: 'web-01' }), { status: 200 }) |
| 394 | + ) |
| 395 | + ).output |
| 396 | + expect(entity.entity.entityId).toBe('HOST-1') |
| 397 | + |
| 398 | + const closed = ( |
| 399 | + await closeProblemTool.transformResponse!( |
| 400 | + new Response( |
| 401 | + JSON.stringify({ |
| 402 | + problemId: 'P-1', |
| 403 | + closeTimestamp: 123, |
| 404 | + closing: true, |
| 405 | + comment: { id: 'C-1', content: 'fixed' }, |
| 406 | + }), |
| 407 | + { status: 200 } |
| 408 | + ) |
| 409 | + ) |
| 410 | + ).output |
| 411 | + expect(closed.problemId).toBe('P-1') |
| 412 | + expect(closed.comment?.content).toBe('fixed') |
| 413 | + }) |
| 414 | + |
249 | 415 | it('surfaces a 200 partial-success log ingestion body', async () => { |
250 | 416 | const response = new Response(JSON.stringify({ error: { message: 'some invalid' } }), { |
251 | 417 | status: 200, |
|
0 commit comments