Pulse/internal/api/ai_patrol_handlers_test.go
rcourtman d3eb6a7148 test: improve test coverage for AI, license, config, and monitoring packages
New test files:
- internal/ai/providers/gemini_test.go: Comprehensive Gemini provider tests
- internal/api/ai_intelligence_handlers_test.go: AI intelligence endpoint tests
- internal/api/ai_patrol_handlers_test.go: AI patrol endpoint tests
- internal/api/license_handlers_test.go: License API handler tests
- internal/api/security_oidc_response_test.go: OIDC response formatting tests
- internal/config/ai_config_test.go: AI configuration function tests
- internal/config/persistence_ai_test.go: AI config persistence tests
- internal/config/persistence_extended_test.go: Extended persistence tests
- internal/license/persistence_test.go: License persistence tests
- internal/license/pubkey_test.go: Public key handling tests
- internal/monitoring/host_agent_temps_test.go: Temperature processing tests

Enhanced existing files:
- internal/api/updates_test.go: Added update handler tests
- internal/license/license_test.go: Added Service method tests

Coverage improvements:
- ai/providers: 57.3% -> 73.0% (+15.7%)
- license: 78.3% -> 85.9% (+7.6%)
- config: 49.7% -> 53.9% (+4.2%)
- monitoring: 49.8% -> 50.8% (+1.0%)
- api: 28.4% -> 29.8% (+1.4%)
2025-12-19 22:49:30 +00:00

298 lines
8.2 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
func TestHandleGetPatrolStatus_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/status", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolStatus(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetPatrolStatus_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/status", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolStatus(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp PatrolStatusResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// When patrol service is not initialized, should return safe defaults
if resp.Running {
t.Error("expected Running to be false when patrol not initialized")
}
if resp.Enabled {
t.Error("expected Enabled to be false when patrol not initialized")
}
if !resp.Healthy {
t.Error("expected Healthy to be true when patrol not initialized (no issues)")
}
}
func TestHandleGetPatrolFindings_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/findings", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolFindings(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetPatrolFindings_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/findings", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolFindings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
// Should return empty array when patrol not initialized
var findings []interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &findings); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(findings) != 0 {
t.Errorf("expected empty findings, got %d", len(findings))
}
}
func TestHandleGetPatrolFindings_WithResourceIDFilter(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/findings?resource_id=vm-100", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolFindings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
}
func TestHandleForcePatrol_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/run", nil)
rec := httptest.NewRecorder()
handler.HandleForcePatrol(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for GET, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleAcknowledgeFinding_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/acknowledge", nil)
rec := httptest.NewRecorder()
handler.HandleAcknowledgeFinding(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for GET, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleSnoozeFinding_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/snooze", nil)
rec := httptest.NewRecorder()
handler.HandleSnoozeFinding(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for GET, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleResolveFinding_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/resolve", nil)
rec := httptest.NewRecorder()
handler.HandleResolveFinding(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for GET, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleDismissFinding_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/dismiss", nil)
rec := httptest.NewRecorder()
handler.HandleDismissFinding(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for GET, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleDismissFinding_InvalidReason(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
// Set up auth - needed for authenticated handlers
InitSessionStore(tmp)
handler := NewAISettingsHandler(cfg, persistence, nil)
// Invalid dismiss reason
body, _ := json.Marshal(map[string]string{
"finding_id": "test-finding",
"reason": "invalid_reason_value",
})
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/dismiss", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleDismissFinding(rec, req)
// Should fail because patrol service not available (after auth check)
// But more importantly, validates the request handling path
if rec.Code == http.StatusOK {
t.Error("expected non-OK status for missing patrol service or invalid input")
}
}
func TestHandleSnoozeFinding_DurationValidation(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
InitSessionStore(tmp)
handler := NewAISettingsHandler(cfg, persistence, nil)
// Missing finding_id
body, _ := json.Marshal(map[string]interface{}{
"duration_hours": 24,
})
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/snooze", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleSnoozeFinding(rec, req)
// Should fail auth or patrol checks
if rec.Code == http.StatusOK {
t.Error("expected non-OK status")
}
}
func TestHandleGetFindingsHistory_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/history", nil)
rec := httptest.NewRecorder()
handler.HandleGetFindingsHistory(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetPatrolRunHistory_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/runs", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolRunHistory(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for POST, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleGetPatrolRunHistory_NoPatrolService(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/runs", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolRunHistory(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
// Should return empty array
var history []interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &history); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(history) != 0 {
t.Errorf("expected empty history, got %d", len(history))
}
}
func TestHandleSuppressFinding_MethodNotAllowed(t *testing.T) {
t.Parallel()
handler := createTestAIHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/suppress", nil)
rec := httptest.NewRecorder()
handler.HandleSuppressFinding(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d for GET, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}