Ctrl K
ComunidadGuías…

api/suite_status_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package api

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/TwiN/gatus/v5/config"
	"github.com/TwiN/gatus/v5/config/endpoint"
	"github.com/TwiN/gatus/v5/config/suite"
	"github.com/TwiN/gatus/v5/storage"
	"github.com/TwiN/gatus/v5/storage/store"
	"github.com/TwiN/gatus/v5/watchdog"
)

var (
	suiteTimestamp = time.Now()

	testSuiteEndpoint1 = endpoint.Endpoint{
		Name:                    "endpoint1",
		Group:                   "suite-group",
		URL:                     "https://example.org/endpoint1",
		Method:                  "GET",
		Interval:                30 * time.Second,
		Conditions:              []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 500")},
		NumberOfFailuresInARow:  0,
		NumberOfSuccessesInARow: 0,
	}
	testSuiteEndpoint2 = endpoint.Endpoint{
		Name:                    "endpoint2",
		Group:                   "suite-group",
		URL:                     "https://example.org/endpoint2",
		Method:                  "GET",
		Interval:                30 * time.Second,
		Conditions:              []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 300")},
		NumberOfFailuresInARow:  0,
		NumberOfSuccessesInARow: 0,
	}
	testSuite = suite.Suite{
		Name:     "test-suite",
		Group:    "suite-group",
		Interval: 60 * time.Second,
		Endpoints: []*endpoint.Endpoint{
			&testSuiteEndpoint1,
			&testSuiteEndpoint2,
		},
	}
	testSuccessfulSuiteResult = suite.Result{
		Name:      "test-suite",
		Group:     "suite-group",
		Success:   true,
		Timestamp: suiteTimestamp,
		Duration:  250 * time.Millisecond,
		EndpointResults: []*endpoint.Result{
			{
				Hostname:   "example.org",
				IP:         "127.0.0.1",
				HTTPStatus: 200,
				Success:    true,
				Timestamp:  suiteTimestamp,
				Duration:   100 * time.Millisecond,
				ConditionResults: []*endpoint.ConditionResult{
					{
						Condition: "[STATUS] == 200",
						Success:   true,
					},
					{
						Condition: "[RESPONSE_TIME] < 500",
						Success:   true,
					},
				},
			},
			{
				Hostname:   "example.org",
				IP:         "127.0.0.1",
				HTTPStatus: 200,
				Success:    true,
				Timestamp:  suiteTimestamp,
				Duration:   150 * time.Millisecond,
				ConditionResults: []*endpoint.ConditionResult{
					{
						Condition: "[STATUS] == 200",
						Success:   true,
					},
					{
						Condition: "[RESPONSE_TIME] < 300",
						Success:   true,
					},
				},
			},
		},
	}
	testUnsuccessfulSuiteResult = suite.Result{
		Name:      "test-suite",
		Group:     "suite-group",
		Success:   false,
		Timestamp: suiteTimestamp,
		Duration:  850 * time.Millisecond,
		Errors:    []string{"suite-error-1", "suite-error-2"},
		EndpointResults: []*endpoint.Result{
			{
				Hostname:   "example.org",
				IP:         "127.0.0.1",
				HTTPStatus: 200,
				Success:    true,
				Timestamp:  suiteTimestamp,
				Duration:   100 * time.Millisecond,
				ConditionResults: []*endpoint.ConditionResult{
					{
						Condition: "[STATUS] == 200",
						Success:   true,
					},
					{
						Condition: "[RESPONSE_TIME] < 500",
						Success:   true,
					},
				},
			},
			{
				Hostname:   "example.org",
				IP:         "127.0.0.1",
				HTTPStatus: 500,
				Errors:     []string{"endpoint-error-1"},
				Success:    false,
				Timestamp:  suiteTimestamp,
				Duration:   750 * time.Millisecond,
				ConditionResults: []*endpoint.ConditionResult{
					{
						Condition: "[STATUS] == 200",
						Success:   false,
					},
					{
						Condition: "[RESPONSE_TIME] < 300",
						Success:   false,
					},
				},
			},
		},
	}
)

func TestSuiteStatus(t *testing.T) {
	defer store.Get().Clear()
	defer cache.Clear()
	cfg := &config.Config{
		Metrics: true,
		Suites: []*suite.Suite{
			{
				Name:  "frontend-suite",
				Group: "core",
			},
			{
				Name:  "backend-suite",
				Group: "core",
			},
		},
		Storage: &storage.Config{
			MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
			MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
		},
	}
	watchdog.UpdateSuiteStatus(cfg.Suites[0], &suite.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now(), Name: cfg.Suites[0].Name, Group: cfg.Suites[0].Group})
	watchdog.UpdateSuiteStatus(cfg.Suites[1], &suite.Result{Success: false, Duration: time.Second, Timestamp: time.Now(), Name: cfg.Suites[1].Name, Group: cfg.Suites[1].Group})
	api := New(cfg)
	router := api.Router()
	type Scenario struct {
		Name         string
		Path         string
		ExpectedCode int
		Gzip         bool
	}
	scenarios := []Scenario{
		{
			Name:         "suite-status",
			Path:         "/api/v1/suites/core_frontend-suite/statuses",
			ExpectedCode: http.StatusOK,
		},
		{
			Name:         "suite-status-gzip",
			Path:         "/api/v1/suites/core_frontend-suite/statuses",
			ExpectedCode: http.StatusOK,
			Gzip:         true,
		},
		{
			Name:         "suite-status-pagination",
			Path:         "/api/v1/suites/core_frontend-suite/statuses?page=1&pageSize=20",
			ExpectedCode: http.StatusOK,
		},
		{
			Name:         "suite-status-for-invalid-key",
			Path:         "/api/v1/suites/invalid_key/statuses",
			ExpectedCode: http.StatusNotFound,
		},
	}
	for _, scenario := range scenarios {
		t.Run(scenario.Name, func(t *testing.T) {
			request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
			if scenario.Gzip {
				request.Header.Set("Accept-Encoding", "gzip")
			}
			response, err := router.Test(request)
			if err != nil {
				return
			}
			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)
			}
		})
	}
}

func TestSuiteStatus_SuiteNotInStoreButInConfig(t *testing.T) {
	defer store.Get().Clear()
	defer cache.Clear()
	tests := []struct {
		name         string
		suiteKey     string
		cfg          *config.Config
		expectedCode int
		expectJSON   bool
		expectError  string
	}{
		{
			name:     "suite-not-in-store-but-exists-in-config-enabled",
			suiteKey: "test-group_test-suite",
			cfg: &config.Config{
				Metrics: true,
				Suites: []*suite.Suite{
					{
						Name:    "test-suite",
						Group:   "test-group",
						Enabled: boolPtr(true),
						Endpoints: []*endpoint.Endpoint{
							{
								Name:  "endpoint-1",
								Group: "test-group",
								URL:   "https://example.com",
							},
						},
					},
				},
				Storage: &storage.Config{
					MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
					MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
				},
			},
			expectedCode: http.StatusOK,
			expectJSON:   true,
		},
		{
			name:     "suite-not-in-store-but-exists-in-config-disabled",
			suiteKey: "test-group_disabled-suite",
			cfg: &config.Config{
				Metrics: true,
				Suites: []*suite.Suite{
					{
						Name:    "disabled-suite",
						Group:   "test-group",
						Enabled: boolPtr(false),
					},
				},
				Storage: &storage.Config{
					MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
					MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
				},
			},
			expectedCode: http.StatusOK,
			expectJSON:   true,
		},
		{
			name:     "suite-not-in-store-and-not-in-config",
			suiteKey: "nonexistent_suite",
			cfg: &config.Config{
				Metrics: true,
				Suites: []*suite.Suite{
					{
						Name:  "different-suite",
						Group: "different-group",
					},
				},
				Storage: &storage.Config{
					MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
					MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
				},
			},
			expectedCode: http.StatusNotFound,
			expectError:  "Suite with key 'nonexistent_suite' not found",
		},
		{
			name:     "suite-with-empty-group-in-config",
			suiteKey: "_empty-group-suite",
			cfg: &config.Config{
				Metrics: true,
				Suites: []*suite.Suite{
					{
						Name:  "empty-group-suite",
						Group: "",
					},
				},
				Storage: &storage.Config{
					MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
					MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
				},
			},
			expectedCode: http.StatusOK,
			expectJSON:   true,
		},
		{
			name:     "suite-nil-enabled-defaults-to-true",
			suiteKey: "default_enabled-suite",
			cfg: &config.Config{
				Metrics: true,
				Suites: []*suite.Suite{
					{
						Name:    "enabled-suite",
						Group:   "default",
						Enabled: nil,
					},
				},
				Storage: &storage.Config{
					MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
					MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
				},
			},
			expectedCode: http.StatusOK,
			expectJSON:   true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			api := New(tt.cfg)
			router := api.Router()
			request := httptest.NewRequest("GET", "/api/v1/suites/"+tt.suiteKey+"/statuses", http.NoBody)
			response, err := router.Test(request)
			if err != nil {
				t.Fatalf("Router test failed: %v", err)
			}
			defer response.Body.Close()
			if response.StatusCode != tt.expectedCode {
				t.Errorf("Expected status code %d, got %d", tt.expectedCode, response.StatusCode)
			}
			body, err := io.ReadAll(response.Body)
			if err != nil {
				t.Fatalf("Failed to read response body: %v", err)
			}
			bodyStr := string(body)
			if tt.expectJSON {
				if response.Header.Get("Content-Type") != "application/json" {
					t.Errorf("Expected JSON content type, got %s", response.Header.Get("Content-Type"))
				}
				if len(bodyStr) == 0 || bodyStr[0] != '{' {
					t.Errorf("Expected JSON response, got: %s", bodyStr)
				}
			}
			if tt.expectError != "" {
				if !contains(bodyStr, tt.expectError) {
					t.Errorf("Expected error message '%s' in response, got: %s", tt.expectError, bodyStr)
				}
			}
		})
	}
}

func TestSuiteStatuses(t *testing.T) {
	defer store.Get().Clear()
	defer cache.Clear()
	firstResult := &testSuccessfulSuiteResult
	secondResult := &testUnsuccessfulSuiteResult
	store.Get().InsertSuiteResult(&testSuite, firstResult)
	store.Get().InsertSuiteResult(&testSuite, secondResult)
	// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
	firstResult.Timestamp = time.Time{}
	secondResult.Timestamp = time.Time{}
	for i := range firstResult.EndpointResults {
		firstResult.EndpointResults[i].Timestamp = time.Time{}
	}
	for i := range secondResult.EndpointResults {
		secondResult.EndpointResults[i].Timestamp = time.Time{}
	}
	api := New(&config.Config{
		Metrics: true,
		Storage: &storage.Config{
			MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
			MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
		},
	})
	router := api.Router()
	type Scenario struct {
		Name         string
		Path         string
		ExpectedCode int
		ExpectedBody string
	}
	scenarios := []Scenario{
		{
			Name:         "no-pagination",
			Path:         "/api/v1/suites/statuses",
			ExpectedCode: http.StatusOK,
			ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":true,"timestamp":"0001-01-01T00:00:00Z","duration":250000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 300","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]},{"name":"test-suite","group":"suite-group","success":false,"timestamp":"0001-01-01T00:00:00Z","duration":850000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":500,"hostname":"example.org","duration":750000000,"errors":["endpoint-error-1"],"conditionResults":[{"condition":"[STATUS] == 200","success":false},{"condition":"[RESPONSE_TIME] \u003c 300","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"errors":["suite-error-1","suite-error-2"]}]}]`,
		},
		{
			Name:         "pagination-first-result",
			Path:         "/api/v1/suites/statuses?page=1&pageSize=1",
			ExpectedCode: http.StatusOK,
			ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":false,"timestamp":"0001-01-01T00:00:00Z","duration":850000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":500,"hostname":"example.org","duration":750000000,"errors":["endpoint-error-1"],"conditionResults":[{"condition":"[STATUS] == 200","success":false},{"condition":"[RESPONSE_TIME] \u003c 300","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"errors":["suite-error-1","suite-error-2"]}]}]`,
		},
		{
			Name:         "pagination-second-result",
			Path:         "/api/v1/suites/statuses?page=2&pageSize=1",
			ExpectedCode: http.StatusOK,
			ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":true,"timestamp":"0001-01-01T00:00:00Z","duration":250000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 300","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]}]}]`,
		},
		{
			Name:         "pagination-no-results",
			Path:         "/api/v1/suites/statuses?page=5&pageSize=20",
			ExpectedCode: http.StatusOK,
			ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[]}]`,
		},
		{
			Name:         "invalid-pagination-should-fall-back-to-default",
			Path:         "/api/v1/suites/statuses?page=INVALID&pageSize=INVALID",
			ExpectedCode: http.StatusOK,
			ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":true,"timestamp":"0001-01-01T00:00:00Z","duration":250000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 300","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]},{"name":"test-suite","group":"suite-group","success":false,"timestamp":"0001-01-01T00:00:00Z","duration":850000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":500,"hostname":"example.org","duration":750000000,"errors":["endpoint-error-1"],"conditionResults":[{"condition":"[STATUS] == 200","success":false},{"condition":"[RESPONSE_TIME] \u003c 300","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"errors":["suite-error-1","suite-error-2"]}]}]`,
		},
	}
	for _, scenario := range scenarios {
		t.Run(scenario.Name, func(t *testing.T) {
			request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
			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)
			}
			body, err := io.ReadAll(response.Body)
			if err != nil {
				t.Error("expected err to be nil, but was", err)
			}
			if string(body) != scenario.ExpectedBody {
				t.Errorf("expected:\n %s\n\ngot:\n %s", scenario.ExpectedBody, string(body))
			}
		})
	}
}

func TestSuiteStatuses_NoSuitesInStoreButExistInConfig(t *testing.T) {
	defer store.Get().Clear()
	defer cache.Clear()
	cfg := &config.Config{
		Metrics: true,
		Suites: []*suite.Suite{
			{
				Name:    "config-only-suite-1",
				Group:   "test-group",
				Enabled: boolPtr(true),
			},
			{
				Name:    "config-only-suite-2",
				Group:   "test-group",
				Enabled: boolPtr(true),
			},
			{
				Name:    "disabled-suite",
				Group:   "test-group",
				Enabled: boolPtr(false),
			},
		},
		Storage: &storage.Config{
			MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
			MaximumNumberOfEvents:  storage.DefaultMaximumNumberOfEvents,
		},
	}
	api := New(cfg)
	router := api.Router()
	request := httptest.NewRequest("GET", "/api/v1/suites/statuses", http.NoBody)
	response, err := router.Test(request)
	if err != nil {
		t.Fatalf("Router test failed: %v", err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode)
	}
	body, err := io.ReadAll(response.Body)
	if err != nil {
		t.Fatalf("Failed to read response body: %v", err)
	}
	bodyStr := string(body)
	if !contains(bodyStr, "config-only-suite-1") {
		t.Error("Expected config-only-suite-1 in response")
	}
	if !contains(bodyStr, "config-only-suite-2") {
		t.Error("Expected config-only-suite-2 in response")
	}
	if contains(bodyStr, "disabled-suite") {
		t.Error("Should not include disabled-suite in response")
	}
}

func boolPtr(b bool) *bool {
	return &b
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
		(len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
			func() bool {
				for i := 1; i <= len(s)-len(substr); i++ {
					if s[i:i+len(substr)] == substr {
						return true
					}
				}
				return false
			}())))
}