From 6352e8c33c1c22af382093d406d477d1530950db Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 12 Jun 2025 21:07:31 +0300 Subject: Add comprehensive unit tests for federated feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tests successful federation from remote endpoints - Tests error handling (server errors, invalid JSON, timeouts) - Tests multiple endpoint federation - Tests duplicate check name conflict resolution - Uses httptest.NewServer for realistic HTTP simulation - Validates federated check marking and status reporting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 82 +++++++++++++ internal/federated_test.go | 280 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 CLAUDE.md create mode 100644 internal/federated_test.go diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb81d6b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Gogios is a lightweight, minimalistic monitoring tool written in Go designed for small-scale server monitoring. It executes Nagios-compatible check plugins and sends email notifications when service states change. + +## Build and Development Commands + +### Build +```bash +# Standard build +go build -o gogios cmd/gogios/main.go + +# Using taskfile +task build + +# Development build with race detection +task dev +``` + +### Code Quality +```bash +# Run go vet +task vet + +# Run linter (requires golangci-lint) +task lint + +# Install linter +task lint-install +``` + +## Architecture + +### Core Components + +- **cmd/gogios/main.go**: Entry point with CLI argument parsing +- **internal/run.go**: Main execution orchestrator +- **internal/config.go**: JSON configuration file parsing and validation +- **internal/check.go**: Individual check execution using external Nagios plugins +- **internal/state.go**: Persistent state management (tracks check status changes) +- **internal/runchecks.go**: Concurrent check execution with dependency handling +- **internal/notify.go**: Email notification system +- **internal/federated.go**: Federation support for distributed monitoring +- **internal/nagioscode.go**: Nagios exit code constants (OK, WARNING, CRITICAL, UNKNOWN) +- **internal/dependency.go**: Check dependency resolution + +### Key Design Patterns + +1. **State-based Notifications**: Only sends emails when check status changes, not on every run +2. **Dependency Checks**: Supports check dependencies (e.g., don't check HTTP if ping fails) +3. **Concurrent Execution**: Runs multiple checks simultaneously with configurable concurrency +4. **Retry Logic**: Configurable retry attempts with intervals for failed checks +5. **Federation**: Can merge results from multiple Gogios instances +6. **Stale Detection**: Identifies checks that haven't run within threshold time + +### Configuration + +The tool is configured via JSON file (typically `/etc/gogios.json`) with: +- Email settings (from/to addresses, SMTP server) +- Check definitions (plugin path, arguments, dependencies) +- Concurrency and timeout settings +- State persistence directory + +### Execution Flow + +1. Load configuration and validate check dependencies +2. Load previous state from state.json +3. Execute checks concurrently (respecting dependencies) +4. Merge federated results if configured +5. Generate report and send notifications for status changes +6. Persist new state + +## Development Notes + +- Uses standard Go project structure with internal package +- No external dependencies beyond Go standard library +- Designed for Unix-like systems (tested on OpenBSD) +- Supports cross-compilation for different platforms +- State persisted as JSON in configurable directory \ No newline at end of file diff --git a/internal/federated_test.go b/internal/federated_test.go new file mode 100644 index 0000000..bfca309 --- /dev/null +++ b/internal/federated_test.go @@ -0,0 +1,280 @@ +package internal + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestMergeFederated(t *testing.T) { + tests := []struct { + name string + serverResponse map[string]checkState + serverStatus int + expectedChecks []string + expectError bool + }{ + { + name: "successful federation", + serverResponse: map[string]checkState{ + "Remote Check 1": { + Status: nagiosOk, + Epoch: time.Now().Unix(), + }, + "Remote Check 2": { + Status: nagiosCritical, + Epoch: time.Now().Unix(), + }, + }, + serverStatus: 200, + expectedChecks: []string{"Remote Check 1", "Remote Check 2"}, + expectError: false, + }, + { + name: "server error", + serverResponse: nil, + serverStatus: 500, + expectedChecks: []string{}, + expectError: true, + }, + { + name: "invalid json", + serverResponse: nil, + serverStatus: 200, + expectedChecks: []string{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tt.serverStatus != 200 { + w.WriteHeader(tt.serverStatus) + return + } + + if tt.serverResponse == nil && tt.name == "invalid json" { + w.Write([]byte("invalid json")) + return + } + + if tt.serverResponse != nil { + jsonData, _ := json.Marshal(tt.serverResponse) + w.Header().Set("Content-Type", "application/json") + w.Write(jsonData) + } + })) + defer server.Close() + + // Create initial state + state := state{ + checks: make(map[string]checkState), + staleEpoch: time.Now().Unix() - 3600, + } + + // Create config with federated endpoint + conf := config{ + Federated: []string{server.URL}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Execute federation + resultState := mergeFederated(ctx, state, conf) + + // Verify results + if tt.expectError { + // Should have a federated endpoint check that failed + federatedCheckName := "Federated endpoint " + server.URL + if check, exists := resultState.checks[federatedCheckName]; !exists { + t.Errorf("Expected federated endpoint check to exist") + } else if check.Status != nagiosCritical { + t.Errorf("Expected federated endpoint check to be CRITICAL, got %v", check.Status) + } + } else { + // Should have merged the remote checks + for _, checkName := range tt.expectedChecks { + if _, exists := resultState.checks[checkName]; !exists { + t.Errorf("Expected check '%s' to be merged", checkName) + } + } + + // Should have successful federated endpoint check + federatedCheckName := "Federated endpoint " + server.URL + if check, exists := resultState.checks[federatedCheckName]; !exists { + t.Errorf("Expected federated endpoint check to exist") + } else if check.Status != nagiosOk { + t.Errorf("Expected federated endpoint check to be OK, got %v", check.Status) + } + } + }) + } +} + +func TestMergeFederatedMultipleEndpoints(t *testing.T) { + // Create two mock servers + server1Response := map[string]checkState{ + "Server1 Check": { + Status: nagiosOk, + Epoch: time.Now().Unix(), + }, + } + + server2Response := map[string]checkState{ + "Server2 Check": { + Status: nagiosWarning, + Epoch: time.Now().Unix(), + }, + } + + server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jsonData, _ := json.Marshal(server1Response) + w.Header().Set("Content-Type", "application/json") + w.Write(jsonData) + })) + defer server1.Close() + + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jsonData, _ := json.Marshal(server2Response) + w.Header().Set("Content-Type", "application/json") + w.Write(jsonData) + })) + defer server2.Close() + + // Create initial state + state := state{ + checks: make(map[string]checkState), + staleEpoch: time.Now().Unix() - 3600, + } + + // Create config with multiple federated endpoints + conf := config{ + Federated: []string{server1.URL, server2.URL}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Execute federation + resultState := mergeFederated(ctx, state, conf) + + // Verify both servers' checks were merged + if _, exists := resultState.checks["Server1 Check"]; !exists { + t.Errorf("Expected Server1 Check to be merged") + } + + if _, exists := resultState.checks["Server2 Check"]; !exists { + t.Errorf("Expected Server2 Check to be merged") + } + + // Verify both federated endpoint checks exist and are OK + federatedCheck1 := "Federated endpoint " + server1.URL + federatedCheck2 := "Federated endpoint " + server2.URL + + if check, exists := resultState.checks[federatedCheck1]; !exists { + t.Errorf("Expected federated endpoint check 1 to exist") + } else if check.Status != nagiosOk { + t.Errorf("Expected federated endpoint check 1 to be OK, got %v", check.Status) + } + + if check, exists := resultState.checks[federatedCheck2]; !exists { + t.Errorf("Expected federated endpoint check 2 to exist") + } else if check.Status != nagiosOk { + t.Errorf("Expected federated endpoint check 2 to be OK, got %v", check.Status) + } +} + +func TestMergeFederatedTimeout(t *testing.T) { + // Create a server that delays response + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Second) + w.WriteHeader(200) + })) + defer server.Close() + + // Create initial state + state := state{ + checks: make(map[string]checkState), + staleEpoch: time.Now().Unix() - 3600, + } + + // Create config with federated endpoint + conf := config{ + Federated: []string{server.URL}, + } + + // Create context with short timeout + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + // Execute federation + resultState := mergeFederated(ctx, state, conf) + + // Should have a failed federated endpoint check + federatedCheckName := "Federated endpoint " + server.URL + if check, exists := resultState.checks[federatedCheckName]; !exists { + t.Errorf("Expected federated endpoint check to exist") + } else if check.Status != nagiosCritical { + t.Errorf("Expected federated endpoint check to be CRITICAL due to timeout, got %v", check.Status) + } +} + +func TestMergeFederatedDuplicateCheckNames(t *testing.T) { + // Create server with check name that conflicts with local state + serverResponse := map[string]checkState{ + "Duplicate Check": { + Status: nagiosOk, + Epoch: time.Now().Unix(), + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jsonData, _ := json.Marshal(serverResponse) + w.Header().Set("Content-Type", "application/json") + w.Write(jsonData) + })) + defer server.Close() + + // Create initial state with existing check + state := state{ + checks: map[string]checkState{ + "Duplicate Check": { + Status: nagiosCritical, + Epoch: time.Now().Unix(), + }, + }, + staleEpoch: time.Now().Unix() - 3600, + } + + // Create config with federated endpoint + conf := config{ + Federated: []string{server.URL}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Execute federation + resultState := mergeFederated(ctx, state, conf) + + // Should have a failed federated endpoint check due to duplicate name + federatedCheckName := "Federated endpoint " + server.URL + if check, exists := resultState.checks[federatedCheckName]; !exists { + t.Errorf("Expected federated endpoint check to exist") + } else if check.Status != nagiosCritical { + t.Errorf("Expected federated endpoint check to be CRITICAL due to duplicate name, got %v", check.Status) + } + + // Original check should remain unchanged + if check, exists := resultState.checks["Duplicate Check"]; !exists { + t.Errorf("Expected original check to remain") + } else if check.Status != nagiosCritical { + t.Errorf("Expected original check status to remain CRITICAL, got %v", check.Status) + } +} \ No newline at end of file -- cgit v1.2.3