* fix(maintenance): validate maintenance windows for external, DNS and SSH endpoints ExternalEndpoint.ValidateAndSetDefaults never called maintenance.Config.ValidateAndSetDefaults, so the window's start time and timezone were never parsed. IsUnderMaintenance then treated every window as starting at midnight in the server's local timezone, so alerts were still sent during the configured window. Endpoint.ValidateAndSetDefaults had the same problem for DNS and SSH endpoints, which returned before reaching the maintenance window loop. The loop now runs before those early returns. Fixes #1551 Made by an AI agent (Claude Code, model Claude Opus 5). * test(api): assert external endpoint maintenance window suppresses alerting Pushes a failing result for an external endpoint whose maintenance window (Europe/Berlin) is currently active and checks that alerting is skipped, exercising the same maintenance check CreateExternalEndpointResult uses. --------- Co-authored-by: TwiN <twin@linux.com>
408 lines
9.9 KiB
Go
408 lines
9.9 KiB
Go
package endpoint
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
|
"github.com/TwiN/gatus/v5/config/endpoint/heartbeat"
|
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
|
)
|
|
|
|
func TestExternalEndpoint_ValidateAndSetDefaults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint *ExternalEndpoint
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "valid-external-endpoint",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Group: "test-group",
|
|
Token: "valid-token",
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "valid-external-endpoint-with-heartbeat",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "valid-token",
|
|
Heartbeat: heartbeat.Config{
|
|
Interval: 30 * time.Second,
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "missing-token",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Group: "test-group",
|
|
},
|
|
wantErr: ErrExternalEndpointWithNoToken,
|
|
},
|
|
{
|
|
name: "empty-token",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "",
|
|
},
|
|
wantErr: ErrExternalEndpointWithNoToken,
|
|
},
|
|
{
|
|
name: "heartbeat-interval-too-low",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "valid-token",
|
|
Heartbeat: heartbeat.Config{
|
|
Interval: 5 * time.Second, // Less than 10 seconds
|
|
},
|
|
},
|
|
wantErr: ErrExternalEndpointHeartbeatIntervalTooLow,
|
|
},
|
|
{
|
|
name: "heartbeat-interval-exactly-10-seconds",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "valid-token",
|
|
Heartbeat: heartbeat.Config{
|
|
Interval: 10 * time.Second,
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "heartbeat-interval-zero-is-allowed",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "valid-token",
|
|
Heartbeat: heartbeat.Config{
|
|
Interval: 0, // Zero means no heartbeat monitoring
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "missing-name",
|
|
endpoint: &ExternalEndpoint{
|
|
Group: "test-group",
|
|
Token: "valid-token",
|
|
},
|
|
wantErr: ErrEndpointWithNoName,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.endpoint.ValidateAndSetDefaults()
|
|
if tt.wantErr != nil {
|
|
if err == nil {
|
|
t.Errorf("Expected error %v, but got none", tt.wantErr)
|
|
return
|
|
}
|
|
if err.Error() != tt.wantErr.Error() {
|
|
t.Errorf("Expected error %v, got %v", tt.wantErr, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error, but got %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalEndpoint_ValidateAndSetDefaultsWithMaintenanceWindow(t *testing.T) {
|
|
t.Run("invalid-maintenance-window", func(t *testing.T) {
|
|
externalEndpoint := &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "valid-token",
|
|
MaintenanceWindows: []*maintenance.Config{{Start: "25:00", Duration: time.Hour}},
|
|
}
|
|
if err := externalEndpoint.ValidateAndSetDefaults(); err == nil {
|
|
t.Error("expected an error because the maintenance window has an invalid start, got none")
|
|
}
|
|
})
|
|
t.Run("under-maintenance", func(t *testing.T) {
|
|
window := newMaintenanceWindowThatStartedAMinuteAgo(t, "Europe/Berlin")
|
|
externalEndpoint := &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "valid-token",
|
|
MaintenanceWindows: []*maintenance.Config{window},
|
|
}
|
|
if err := externalEndpoint.ValidateAndSetDefaults(); err != nil {
|
|
t.Fatal("did not expect an error, got", err)
|
|
}
|
|
if !externalEndpoint.MaintenanceWindows[0].IsUnderMaintenance() {
|
|
t.Errorf("expected external endpoint to be under maintenance (window starting at %s %s)", window.Start, window.Timezone)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExternalEndpoint_IsEnabled(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enabled *bool
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "nil-enabled-defaults-to-true",
|
|
enabled: nil,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "explicitly-enabled",
|
|
enabled: boolPtr(true),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "explicitly-disabled",
|
|
enabled: boolPtr(false),
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
endpoint := &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "test-token",
|
|
Enabled: tt.enabled,
|
|
}
|
|
result := endpoint.IsEnabled()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalEndpoint_DisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint *ExternalEndpoint
|
|
expected string
|
|
}{
|
|
{
|
|
name: "with-group",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Group: "test-group",
|
|
},
|
|
expected: "test-group/test-endpoint",
|
|
},
|
|
{
|
|
name: "without-group",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Group: "",
|
|
},
|
|
expected: "test-endpoint",
|
|
},
|
|
{
|
|
name: "empty-group-string",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "api-health",
|
|
Group: "",
|
|
},
|
|
expected: "api-health",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.endpoint.DisplayName()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalEndpoint_Key(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint *ExternalEndpoint
|
|
expected string
|
|
}{
|
|
{
|
|
name: "with-group",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Group: "test-group",
|
|
},
|
|
expected: "test-group_test-endpoint",
|
|
},
|
|
{
|
|
name: "without-group",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Group: "",
|
|
},
|
|
expected: "_test-endpoint",
|
|
},
|
|
{
|
|
name: "special-characters-in-name",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test endpoint with spaces",
|
|
Group: "test-group",
|
|
},
|
|
expected: "test-group_test-endpoint-with-spaces",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.endpoint.Key()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalEndpoint_ToEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
externalEndpoint *ExternalEndpoint
|
|
}{
|
|
{
|
|
name: "complete-external-endpoint",
|
|
externalEndpoint: &ExternalEndpoint{
|
|
Enabled: boolPtr(true),
|
|
Name: "test-endpoint",
|
|
Group: "test-group",
|
|
Token: "test-token",
|
|
Alerts: []*alert.Alert{
|
|
{
|
|
Type: alert.TypeSlack,
|
|
},
|
|
},
|
|
MaintenanceWindows: []*maintenance.Config{
|
|
{
|
|
Start: "02:00",
|
|
Duration: time.Hour,
|
|
},
|
|
},
|
|
NumberOfFailuresInARow: 3,
|
|
NumberOfSuccessesInARow: 5,
|
|
},
|
|
},
|
|
{
|
|
name: "minimal-external-endpoint",
|
|
externalEndpoint: &ExternalEndpoint{
|
|
Name: "minimal-endpoint",
|
|
Token: "minimal-token",
|
|
},
|
|
},
|
|
{
|
|
name: "disabled-external-endpoint",
|
|
externalEndpoint: &ExternalEndpoint{
|
|
Enabled: boolPtr(false),
|
|
Name: "disabled-endpoint",
|
|
Token: "disabled-token",
|
|
},
|
|
},
|
|
{
|
|
name: "original-test-case",
|
|
externalEndpoint: &ExternalEndpoint{
|
|
Name: "name",
|
|
Group: "group",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.externalEndpoint.ToEndpoint()
|
|
// Verify all fields are correctly copied
|
|
if result.Enabled != tt.externalEndpoint.Enabled {
|
|
t.Errorf("Expected Enabled=%v, got %v", tt.externalEndpoint.Enabled, result.Enabled)
|
|
}
|
|
if result.Name != tt.externalEndpoint.Name {
|
|
t.Errorf("Expected Name=%q, got %q", tt.externalEndpoint.Name, result.Name)
|
|
}
|
|
if result.Group != tt.externalEndpoint.Group {
|
|
t.Errorf("Expected Group=%q, got %q", tt.externalEndpoint.Group, result.Group)
|
|
}
|
|
if len(result.Alerts) != len(tt.externalEndpoint.Alerts) {
|
|
t.Errorf("Expected %d alerts, got %d", len(tt.externalEndpoint.Alerts), len(result.Alerts))
|
|
}
|
|
if result.NumberOfFailuresInARow != tt.externalEndpoint.NumberOfFailuresInARow {
|
|
t.Errorf("Expected NumberOfFailuresInARow=%d, got %d", tt.externalEndpoint.NumberOfFailuresInARow, result.NumberOfFailuresInARow)
|
|
}
|
|
if result.NumberOfSuccessesInARow != tt.externalEndpoint.NumberOfSuccessesInARow {
|
|
t.Errorf("Expected NumberOfSuccessesInARow=%d, got %d", tt.externalEndpoint.NumberOfSuccessesInARow, result.NumberOfSuccessesInARow)
|
|
}
|
|
// Original test assertions
|
|
if tt.externalEndpoint.Key() != result.Key() {
|
|
t.Errorf("expected %s, got %s", tt.externalEndpoint.Key(), result.Key())
|
|
}
|
|
if tt.externalEndpoint.DisplayName() != result.DisplayName() {
|
|
t.Errorf("expected %s, got %s", tt.externalEndpoint.DisplayName(), result.DisplayName())
|
|
}
|
|
// Verify it's a proper Endpoint type
|
|
if result == nil {
|
|
t.Error("ToEndpoint() returned nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalEndpoint_ValidationEdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint *ExternalEndpoint
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "very-long-name",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "this-is-a-very-long-endpoint-name-that-might-cause-issues-in-some-systems-but-should-be-handled-gracefully",
|
|
Token: "valid-token",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "special-characters-in-name",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint@#$%^&*()",
|
|
Token: "valid-token",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "unicode-characters-in-name",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "测试端点",
|
|
Token: "valid-token",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "very-long-token",
|
|
endpoint: &ExternalEndpoint{
|
|
Name: "test-endpoint",
|
|
Token: "very-long-token-that-should-still-be-valid-even-though-it-is-extremely-long-and-might-not-be-practical-in-real-world-scenarios",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.endpoint.ValidateAndSetDefaults()
|
|
if tt.wantErr && err == nil {
|
|
t.Error("Expected error but got none")
|
|
}
|
|
if !tt.wantErr && err != nil {
|
|
t.Errorf("Expected no error but got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function to create bool pointers
|
|
func boolPtr(b bool) *bool {
|
|
return &b
|
|
}
|