|
6 | 6 | "fmt" |
7 | 7 | "net/http" |
8 | 8 | "net/http/httptest" |
| 9 | + "strings" |
9 | 10 | "sync" |
10 | 11 | "testing" |
11 | 12 | "time" |
@@ -2397,3 +2398,224 @@ if _, exists := raw["extensions"]; exists { |
2397 | 2398 | t.Error("Expected 'extensions' field to be absent when no providers registered") |
2398 | 2399 | } |
2399 | 2400 | } |
| 2401 | + |
| 2402 | +func TestDefender_GetReport_IncludesExtensions(t *testing.T) { |
| 2403 | +d := newTestDefender() |
| 2404 | + |
| 2405 | +d.RegisterStatsProvider(&mockStatsDataProvider{ |
| 2406 | +name: "report-ext", |
| 2407 | +data: map[string]interface{}{"report_metric": 7}, |
| 2408 | +}) |
| 2409 | + |
| 2410 | +req := httptest.NewRequest("GET", "/report?period=1", nil) |
| 2411 | +w := httptest.NewRecorder() |
| 2412 | +d.GetReport(w, req) |
| 2413 | + |
| 2414 | +if w.Code != http.StatusOK { |
| 2415 | +t.Fatalf("Expected 200, got %d", w.Code) |
| 2416 | +} |
| 2417 | + |
| 2418 | +var report Report |
| 2419 | +if err := json.NewDecoder(w.Body).Decode(&report); err != nil { |
| 2420 | +t.Fatalf("Failed to decode report: %v", err) |
| 2421 | +} |
| 2422 | + |
| 2423 | +if report.Extensions == nil { |
| 2424 | +t.Fatal("Expected extensions field to be non-nil") |
| 2425 | +} |
| 2426 | + |
| 2427 | +extData, ok := report.Extensions["report-ext"] |
| 2428 | +if !ok { |
| 2429 | +t.Fatal("Expected 'report-ext' key in extensions") |
| 2430 | +} |
| 2431 | + |
| 2432 | +dataMap, ok := extData.(map[string]interface{}) |
| 2433 | +if !ok { |
| 2434 | +t.Fatalf("Expected map, got %T", extData) |
| 2435 | +} |
| 2436 | + |
| 2437 | +if dataMap["report_metric"] != float64(7) { |
| 2438 | +t.Errorf("Expected report_metric=7, got %v", dataMap["report_metric"]) |
| 2439 | +} |
| 2440 | +} |
| 2441 | + |
| 2442 | +func TestDefender_GetReport_NoExtensions_OmitsField(t *testing.T) { |
| 2443 | +d := newTestDefender() |
| 2444 | + |
| 2445 | +req := httptest.NewRequest("GET", "/report?period=1", nil) |
| 2446 | +w := httptest.NewRecorder() |
| 2447 | +d.GetReport(w, req) |
| 2448 | + |
| 2449 | +if w.Code != http.StatusOK { |
| 2450 | +t.Fatalf("Expected 200, got %d", w.Code) |
| 2451 | +} |
| 2452 | + |
| 2453 | +var raw map[string]interface{} |
| 2454 | +if err := json.NewDecoder(w.Body).Decode(&raw); err != nil { |
| 2455 | +t.Fatalf("Failed to decode report: %v", err) |
| 2456 | +} |
| 2457 | + |
| 2458 | +if _, exists := raw["extensions"]; exists { |
| 2459 | +t.Error("Expected 'extensions' field to be absent when no providers registered") |
| 2460 | +} |
| 2461 | +} |
| 2462 | + |
| 2463 | +func TestDefender_MetricsHandler_IncludesExtensions(t *testing.T) { |
| 2464 | +d := newTestDefender() |
| 2465 | + |
| 2466 | +d.RegisterStatsProvider(&mockStatsDataProvider{ |
| 2467 | +name: "my-provider", |
| 2468 | +data: map[string]interface{}{ |
| 2469 | +"hit_count": int64(123), |
| 2470 | +"string_skip": "ignored", |
| 2471 | +}, |
| 2472 | +}) |
| 2473 | + |
| 2474 | +req := httptest.NewRequest("GET", "/metrics", nil) |
| 2475 | +w := httptest.NewRecorder() |
| 2476 | +d.MetricsHandler(w, req) |
| 2477 | + |
| 2478 | +if w.Code != http.StatusOK { |
| 2479 | +t.Fatalf("Expected 200, got %d", w.Code) |
| 2480 | +} |
| 2481 | + |
| 2482 | +body := w.Body.String() |
| 2483 | + |
| 2484 | +// Numeric value should appear as a Prometheus gauge |
| 2485 | +if !strings.Contains(body, "ops_defender_extension_my_provider_hit_count") { |
| 2486 | +t.Errorf("Expected extension metric 'ops_defender_extension_my_provider_hit_count' in output, got:\n%s", body) |
| 2487 | +} |
| 2488 | + |
| 2489 | +// String values must be skipped |
| 2490 | +if strings.Contains(body, "string_skip") { |
| 2491 | +t.Errorf("Expected string value to be skipped, but found 'string_skip' in output") |
| 2492 | +} |
| 2493 | +} |
| 2494 | + |
| 2495 | +func TestDefender_MetricsHandler_NoExtensions_NoExtraOutput(t *testing.T) { |
| 2496 | +d := newTestDefender() |
| 2497 | + |
| 2498 | +req := httptest.NewRequest("GET", "/metrics", nil) |
| 2499 | +w := httptest.NewRecorder() |
| 2500 | +d.MetricsHandler(w, req) |
| 2501 | + |
| 2502 | +body := w.Body.String() |
| 2503 | + |
| 2504 | +if strings.Contains(body, "ops_defender_extension_") { |
| 2505 | +t.Errorf("Expected no extension metrics with no providers, but found some in output:\n%s", body) |
| 2506 | +} |
| 2507 | +} |
| 2508 | + |
| 2509 | +func TestDefender_TimeSeriesHandler_IncludesExtensions(t *testing.T) { |
| 2510 | +d := newTestDefender() |
| 2511 | + |
| 2512 | +d.RegisterStatsProvider(&mockStatsDataProvider{ |
| 2513 | +name: "ts-ext", |
| 2514 | +data: map[string]interface{}{"ts_counter": 55}, |
| 2515 | +}) |
| 2516 | + |
| 2517 | +req := httptest.NewRequest("GET", "/timeseries?period=1&interval=1h", nil) |
| 2518 | +w := httptest.NewRecorder() |
| 2519 | +d.TimeSeriesHandler(w, req) |
| 2520 | + |
| 2521 | +if w.Code != http.StatusOK { |
| 2522 | +t.Fatalf("Expected 200, got %d", w.Code) |
| 2523 | +} |
| 2524 | + |
| 2525 | +var resp TimeSeriesResponse |
| 2526 | +if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { |
| 2527 | +t.Fatalf("Failed to decode timeseries response: %v", err) |
| 2528 | +} |
| 2529 | + |
| 2530 | +if resp.Extensions == nil { |
| 2531 | +t.Fatal("Expected extensions field to be non-nil") |
| 2532 | +} |
| 2533 | + |
| 2534 | +extData, ok := resp.Extensions["ts-ext"] |
| 2535 | +if !ok { |
| 2536 | +t.Fatal("Expected 'ts-ext' key in extensions") |
| 2537 | +} |
| 2538 | + |
| 2539 | +dataMap, ok := extData.(map[string]interface{}) |
| 2540 | +if !ok { |
| 2541 | +t.Fatalf("Expected map, got %T", extData) |
| 2542 | +} |
| 2543 | + |
| 2544 | +if dataMap["ts_counter"] != float64(55) { |
| 2545 | +t.Errorf("Expected ts_counter=55, got %v", dataMap["ts_counter"]) |
| 2546 | +} |
| 2547 | +} |
| 2548 | + |
| 2549 | +func TestDefender_TimeSeriesHandler_NoExtensions_OmitsField(t *testing.T) { |
| 2550 | +d := newTestDefender() |
| 2551 | + |
| 2552 | +req := httptest.NewRequest("GET", "/timeseries?period=1&interval=1h", nil) |
| 2553 | +w := httptest.NewRecorder() |
| 2554 | +d.TimeSeriesHandler(w, req) |
| 2555 | + |
| 2556 | +if w.Code != http.StatusOK { |
| 2557 | +t.Fatalf("Expected 200, got %d", w.Code) |
| 2558 | +} |
| 2559 | + |
| 2560 | +var raw map[string]interface{} |
| 2561 | +if err := json.NewDecoder(w.Body).Decode(&raw); err != nil { |
| 2562 | +t.Fatalf("Failed to decode timeseries response: %v", err) |
| 2563 | +} |
| 2564 | + |
| 2565 | +if _, exists := raw["extensions"]; exists { |
| 2566 | +t.Error("Expected 'extensions' field to be absent when no providers registered") |
| 2567 | +} |
| 2568 | +} |
| 2569 | + |
| 2570 | +func TestSanitizePrometheusName(t *testing.T) { |
| 2571 | +tests := []struct { |
| 2572 | +input string |
| 2573 | +expected string |
| 2574 | +}{ |
| 2575 | +{"my-provider", "my_provider"}, |
| 2576 | +{"My Provider", "my_provider"}, |
| 2577 | +{"hit.count", "hit_count"}, |
| 2578 | +{"sql-injection-detector", "sql_injection_detector"}, |
| 2579 | +{"already_valid", "already_valid"}, |
| 2580 | +{"has spaces", "has_spaces"}, |
| 2581 | +} |
| 2582 | + |
| 2583 | +for _, tt := range tests { |
| 2584 | +t.Run(tt.input, func(t *testing.T) { |
| 2585 | +got := sanitizePrometheusName(tt.input) |
| 2586 | +if got != tt.expected { |
| 2587 | +t.Errorf("sanitizePrometheusName(%q) = %q, want %q", tt.input, got, tt.expected) |
| 2588 | +} |
| 2589 | +}) |
| 2590 | +} |
| 2591 | +} |
| 2592 | + |
| 2593 | +func TestToFloat64(t *testing.T) { |
| 2594 | +tests := []struct { |
| 2595 | +input interface{} |
| 2596 | +expected float64 |
| 2597 | +ok bool |
| 2598 | +}{ |
| 2599 | +{int(42), 42.0, true}, |
| 2600 | +{int32(10), 10.0, true}, |
| 2601 | +{int64(100), 100.0, true}, |
| 2602 | +{float32(3.14), float64(float32(3.14)), true}, |
| 2603 | +{float64(2.71), 2.71, true}, |
| 2604 | +{uint(5), 5.0, true}, |
| 2605 | +{uint32(7), 7.0, true}, |
| 2606 | +{uint64(9), 9.0, true}, |
| 2607 | +{"string", 0, false}, |
| 2608 | +{true, 0, false}, |
| 2609 | +{nil, 0, false}, |
| 2610 | +} |
| 2611 | + |
| 2612 | +for _, tt := range tests { |
| 2613 | +got, ok := toFloat64(tt.input) |
| 2614 | +if ok != tt.ok { |
| 2615 | +t.Errorf("toFloat64(%v): ok=%v, want %v", tt.input, ok, tt.ok) |
| 2616 | +} |
| 2617 | +if ok && got != tt.expected { |
| 2618 | +t.Errorf("toFloat64(%v) = %v, want %v", tt.input, got, tt.expected) |
| 2619 | +} |
| 2620 | +} |
| 2621 | +} |
0 commit comments