* 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>
228 lines
7.8 KiB
Go
228 lines
7.8 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/TwiN/gatus/v5/alerting"
|
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
|
"github.com/TwiN/gatus/v5/config"
|
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
|
"github.com/TwiN/gatus/v5/storage/store"
|
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
|
)
|
|
|
|
func TestCreateExternalEndpointResult(t *testing.T) {
|
|
defer store.Get().Clear()
|
|
defer cache.Clear()
|
|
cfg := &config.Config{
|
|
Alerting: &alerting.Config{
|
|
Discord: &discord.AlertProvider{},
|
|
},
|
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
|
{
|
|
Name: "n",
|
|
Group: "g",
|
|
Token: "token",
|
|
Alerts: []*alert.Alert{
|
|
{
|
|
Type: alert.TypeDiscord,
|
|
FailureThreshold: 2,
|
|
SuccessThreshold: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Maintenance: &maintenance.Config{},
|
|
}
|
|
api := New(cfg)
|
|
router := api.Router()
|
|
scenarios := []struct {
|
|
Name string
|
|
Path string
|
|
AuthorizationHeaderBearerToken string
|
|
ExpectedCode int
|
|
}{
|
|
{
|
|
Name: "no-token",
|
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
|
AuthorizationHeaderBearerToken: "",
|
|
ExpectedCode: 401,
|
|
},
|
|
{
|
|
Name: "bad-token",
|
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
|
AuthorizationHeaderBearerToken: "Bearer bad-token",
|
|
ExpectedCode: 401,
|
|
},
|
|
{
|
|
Name: "bad-key",
|
|
Path: "/api/v1/endpoints/bad_key/external?success=true",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 404,
|
|
},
|
|
{
|
|
Name: "bad-success-value",
|
|
Path: "/api/v1/endpoints/g_n/external?success=invalid",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 400,
|
|
},
|
|
{
|
|
Name: "bad-duration-value",
|
|
Path: "/api/v1/endpoints/g_n/external?success=true&duration=invalid",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 400,
|
|
},
|
|
{
|
|
Name: "good-token-success-true",
|
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 200,
|
|
},
|
|
{
|
|
Name: "good-token-success-true-with-ignored-error-because-success-true",
|
|
Path: "/api/v1/endpoints/g_n/external?success=true&error=failed",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 200,
|
|
},
|
|
{
|
|
Name: "good-duration-success-true",
|
|
Path: "/api/v1/endpoints/g_n/external?success=true&duration=10s",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 200,
|
|
},
|
|
{
|
|
Name: "good-token-success-false",
|
|
Path: "/api/v1/endpoints/g_n/external?success=false",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 200,
|
|
},
|
|
{
|
|
Name: "good-token-success-false-again",
|
|
Path: "/api/v1/endpoints/g_n/external?success=false",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 200,
|
|
},
|
|
{
|
|
Name: "good-token-success-false-with-error",
|
|
Path: "/api/v1/endpoints/g_n/external?success=false&error=failed",
|
|
AuthorizationHeaderBearerToken: "Bearer token",
|
|
ExpectedCode: 200,
|
|
},
|
|
}
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.Name, func(t *testing.T) {
|
|
request := httptest.NewRequest("POST", scenario.Path, http.NoBody)
|
|
if len(scenario.AuthorizationHeaderBearerToken) > 0 {
|
|
request.Header.Set("Authorization", scenario.AuthorizationHeaderBearerToken)
|
|
}
|
|
response, err := router.Test(request)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != scenario.ExpectedCode {
|
|
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
t.Run("verify-end-results", func(t *testing.T) {
|
|
endpointStatus, err := store.Get().GetEndpointStatus("g", "n", paging.NewEndpointStatusParams().WithResults(1, 11))
|
|
if err != nil {
|
|
t.Errorf("failed to get endpoint status: %s", err.Error())
|
|
return
|
|
}
|
|
if endpointStatus.Key != "g_n" {
|
|
t.Errorf("expected key to be g_n but got %s", endpointStatus.Key)
|
|
}
|
|
if len(endpointStatus.Results) != 6 {
|
|
t.Errorf("expected 6 results but got %d", len(endpointStatus.Results))
|
|
}
|
|
if !endpointStatus.Results[0].Success {
|
|
t.Errorf("expected first result to be successful")
|
|
}
|
|
if !endpointStatus.Results[1].Success {
|
|
t.Errorf("expected second result to be successful")
|
|
}
|
|
if len(endpointStatus.Results[1].Errors) > 0 {
|
|
t.Errorf("expected second result to have no errors")
|
|
}
|
|
if endpointStatus.Results[2].Duration == 0 || endpointStatus.Results[2].Duration.Seconds() != 10 {
|
|
t.Errorf("expected third result to have a duration of 10 seconds")
|
|
}
|
|
if endpointStatus.Results[3].Success {
|
|
t.Errorf("expected fourth result to be unsuccessful")
|
|
}
|
|
if endpointStatus.Results[4].Success {
|
|
t.Errorf("expected fifth result to be unsuccessful")
|
|
}
|
|
if endpointStatus.Results[5].Success {
|
|
t.Errorf("expected sixth result to be unsuccessful")
|
|
}
|
|
if len(endpointStatus.Results[5].Errors) == 0 || endpointStatus.Results[5].Errors[0] != "failed" {
|
|
t.Errorf("expected sixth result to have errors: failed")
|
|
}
|
|
externalEndpointFromConfig := cfg.GetExternalEndpointByKey("g_n")
|
|
if externalEndpointFromConfig.NumberOfFailuresInARow != 3 {
|
|
t.Errorf("expected 3 failures in a row but got %d", externalEndpointFromConfig.NumberOfFailuresInARow)
|
|
}
|
|
if externalEndpointFromConfig.NumberOfSuccessesInARow != 0 {
|
|
t.Errorf("expected 0 successes in a row but got %d", externalEndpointFromConfig.NumberOfSuccessesInARow)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCreateExternalEndpointResultUnderEndpointMaintenanceWindow(t *testing.T) {
|
|
defer store.Get().Clear()
|
|
defer cache.Clear()
|
|
location, err := time.LoadLocation("Europe/Berlin")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
externalEndpoint := &endpoint.ExternalEndpoint{
|
|
Name: "n",
|
|
Group: "g",
|
|
Token: "token",
|
|
Alerts: []*alert.Alert{
|
|
{
|
|
Type: alert.TypeDiscord,
|
|
FailureThreshold: 2,
|
|
SuccessThreshold: 2,
|
|
},
|
|
},
|
|
MaintenanceWindows: []*maintenance.Config{{
|
|
Start: time.Now().In(location).Add(-time.Minute).Format("15:04"),
|
|
Duration: time.Hour,
|
|
Timezone: "Europe/Berlin",
|
|
}},
|
|
}
|
|
if err := externalEndpoint.ValidateAndSetDefaults(); err != nil {
|
|
t.Fatal("did not expect an error, got", err)
|
|
}
|
|
cfg := &config.Config{
|
|
Alerting: &alerting.Config{
|
|
Discord: &discord.AlertProvider{},
|
|
},
|
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{externalEndpoint},
|
|
Maintenance: &maintenance.Config{},
|
|
}
|
|
router := New(cfg).Router()
|
|
request := httptest.NewRequest("POST", "/api/v1/endpoints/g_n/external?success=false", http.NoBody)
|
|
request.Header.Set("Authorization", "Bearer token")
|
|
response, err := router.Test(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != 200 {
|
|
t.Fatalf("expected 200, got %d", response.StatusCode)
|
|
}
|
|
if externalEndpoint.NumberOfFailuresInARow != 0 {
|
|
t.Errorf("expected alerting to be skipped because the endpoint is under maintenance, but NumberOfFailuresInARow is %d", externalEndpoint.NumberOfFailuresInARow)
|
|
}
|
|
}
|