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
2 changes: 2 additions & 0 deletions sre-agent/hub/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ module github.com/Vaishnav88sk/claritty/sre-agent/hub
go 1.24

require github.com/lib/pq v1.10.9

require github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
3 changes: 3 additions & 0 deletions sre-agent/hub/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
215 changes: 215 additions & 0 deletions sre-agent/hub/internal/api/handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package api

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/DATA-DOG/go-sqlmock"
"github.com/Vaishnav88sk/claritty/sre-agent/hub/internal/db"
"github.com/Vaishnav88sk/claritty/sre-agent/hub/internal/slack"
)

func setupTestApp(t *testing.T) (*http.ServeMux, sqlmock.Sqlmock) {
// Create sqlmock database connection
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to open sqlmock: %v", err)
}

// Create store using the mock DB
store := db.NewWithDB(mockDB)

// Create a dummy slack client
slackClient := slack.New("dummy-token", "dummy-channel")

// Create handler and register routes
handler := New(store, slackClient, "http://dummy-hub", "secret-key")
mux := http.ServeMux{}
handler.RegisterRoutes(&mux)

return &mux, mock
}

func TestReceiveIncident_Valid(t *testing.T) {
mux, mock := setupTestApp(t)

// Mock the DB expectations for UpsertCluster
mock.ExpectExec(`INSERT INTO clusters`).
WillReturnResult(sqlmock.NewResult(1, 1))

// Mock the DB expectations for InsertIncident (including cluster upsert check)
mock.ExpectExec(`INSERT INTO clusters`).
WillReturnResult(sqlmock.NewResult(1, 1))

mock.ExpectExec(`INSERT INTO incidents`).
WillReturnResult(sqlmock.NewResult(1, 1))

payload := map[string]interface{}{
"id": "inc-123",
"cluster_name": "test-cluster",
"severity": "SEV1",
"title": "CrashLoopBackOff in DB",
"llm_model": "gpt-4",
"has_issue": true,
"confidence_score": 95,
"detected_at": time.Now().Format(time.RFC3339),
}

body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/api/v1/incidents", bytes.NewBuffer(body))
req.Header.Set("X-Claritty-Key", "secret-key")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", w.Code)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled DB expectations: %s", err)
}
}

func TestReceiveIncident_InvalidJSON(t *testing.T) {
mux, _ := setupTestApp(t)

req := httptest.NewRequest(http.MethodPost, "/api/v1/incidents", bytes.NewBufferString("{invalid-json}"))
req.Header.Set("X-Claritty-Key", "secret-key")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusBadRequest {
t.Errorf("Expected status 400, got %d", w.Code)
}
}

func TestReceiveIncident_Unauthorized(t *testing.T) {
mux, _ := setupTestApp(t)

payload := map[string]interface{}{"id": "inc-123"}
body, _ := json.Marshal(payload)

req := httptest.NewRequest(http.MethodPost, "/api/v1/incidents", bytes.NewBuffer(body))
// NOT setting X-Claritty-Key
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status 401, got %d", w.Code)
}
}

func TestGetStats(t *testing.T) {
mux, mock := setupTestApp(t)

// Mock DB queries in store.GetStats()
mock.ExpectQuery(`SELECT COUNT\(\*\) FROM incidents$`).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(10))

mock.ExpectQuery(`SELECT COUNT\(\*\) FROM incidents WHERE status='INVESTIGATING'`).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(3))

mock.ExpectQuery(`SELECT COALESCE`).
WillReturnRows(sqlmock.NewRows([]string{"mttr"}).AddRow(3600.5))

req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil)
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}

var response map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}

if response["total_incidents"] != float64(10) {
t.Errorf("Expected total_incidents=10, got %v", response["total_incidents"])
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled DB expectations: %s", err)
}
}

func TestListIncidents(t *testing.T) {
mux, mock := setupTestApp(t)

// Mock DB expectation
mock.ExpectQuery(`SELECT id, cluster, namespace, severity, status, title, category, root_cause`).
WillReturnRows(sqlmock.NewRows([]string{
"id", "cluster", "namespace", "severity", "status", "title", "category", "root_cause",
"contributing_factors", "affected_services", "affected_namespaces", "remediation_plan",
"llm_model", "confidence", "has_issue", "detected_at", "resolved_at", "scan_duration_secs",
}).AddRow(
"inc-1", "cluster-a", "default", "SEV1", "INVESTIGATING", "Pod Crash", "Compute", "OOM",
"[]", "[]", "[]", "[]", "gpt-4", 90, true, time.Now(), nil, 0.0,
))

req := httptest.NewRequest(http.MethodGet, "/api/v1/incidents", nil)
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}

func TestGetIncidentByID(t *testing.T) {
mux, mock := setupTestApp(t)

// Mock DB expectation
mock.ExpectQuery(`SELECT id, cluster, namespace, severity, status, title, category, root_cause`).
WithArgs("inc-123").
WillReturnRows(sqlmock.NewRows([]string{
"id", "cluster", "namespace", "severity", "status", "title", "category", "root_cause",
"contributing_factors", "affected_services", "affected_namespaces", "remediation_plan",
"llm_model", "confidence", "has_issue", "detected_at", "resolved_at", "scan_duration_secs",
}).AddRow(
"inc-123", "cluster-a", "default", "SEV1", "INVESTIGATING", "Pod Crash", "Compute", "OOM",
"[]", "[]", "[]", "[]", "gpt-4", 90, true, time.Now(), nil, 0.0,
))

req := httptest.NewRequest(http.MethodGet, "/api/v1/incidents/inc-123", nil)
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}

func TestListClusters(t *testing.T) {
mux, mock := setupTestApp(t)

// Mock DB expectation
mock.ExpectQuery(`SELECT name, last_seen, health_score, ready_nodes, total_nodes, running_pods, pending_pods, failed_pods, crashloop, namespaces FROM clusters`).
WillReturnRows(sqlmock.NewRows([]string{
"name", "last_seen", "health_score", "ready_nodes", "total_nodes", "running_pods", "pending_pods", "failed_pods", "crashloop", "namespaces",
}).AddRow(
"cluster-a", time.Now(), 98.5, 3, 3, 100, 0, 0, 0, "[\"default\"]",
))

req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters", nil)
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}
6 changes: 6 additions & 0 deletions sre-agent/hub/internal/db/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ func New(databaseURL string) (*Store, error) {
return s, nil
}

// NewWithDB creates a Store with an existing sql.DB connection.
// Useful for testing with tools like go-sqlmock.
func NewWithDB(db *sql.DB) *Store {
return &Store{db: db}
}

func (s *Store) migrate() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS clusters (
Expand Down
Loading