mirror of
https://github.com/TwiN/gatus.git
synced 2026-08-22 15:16:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a06a74cc3 | ||
|
|
d6fa2c955b |
@@ -565,13 +565,21 @@ func (e *Endpoint) buildHTTPRequest() *http.Request {
|
||||
return request
|
||||
}
|
||||
|
||||
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
||||
// needsToReadBody checks if there's any condition or store mapping that requires the response Body to be read
|
||||
func (e *Endpoint) needsToReadBody() bool {
|
||||
for _, condition := range e.Conditions {
|
||||
if condition.hasBodyPlaceholder() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Check store values for body placeholders
|
||||
if e.Store != nil {
|
||||
for _, value := range e.Store {
|
||||
if strings.Contains(value, BodyPlaceholder) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -914,6 +914,40 @@ func TestEndpoint_needsToReadBody(t *testing.T) {
|
||||
if !(&Endpoint{Conditions: []Condition{bodyConditionWithLength, statusCondition}}).needsToReadBody() {
|
||||
t.Error("expected true, got false")
|
||||
}
|
||||
// Test store configuration with body placeholder
|
||||
storeWithBodyPlaceholder := map[string]string{
|
||||
"token": "[BODY].accessToken",
|
||||
}
|
||||
if !(&Endpoint{
|
||||
Conditions: []Condition{statusCondition},
|
||||
Store: storeWithBodyPlaceholder,
|
||||
}).needsToReadBody() {
|
||||
t.Error("expected true when store has body placeholder, got false")
|
||||
}
|
||||
// Test store configuration without body placeholder
|
||||
storeWithoutBodyPlaceholder := map[string]string{
|
||||
"status": "[STATUS]",
|
||||
}
|
||||
if (&Endpoint{
|
||||
Conditions: []Condition{statusCondition},
|
||||
Store: storeWithoutBodyPlaceholder,
|
||||
}).needsToReadBody() {
|
||||
t.Error("expected false when store has no body placeholder, got true")
|
||||
}
|
||||
// Test empty store
|
||||
if (&Endpoint{
|
||||
Conditions: []Condition{statusCondition},
|
||||
Store: map[string]string{},
|
||||
}).needsToReadBody() {
|
||||
t.Error("expected false when store is empty, got true")
|
||||
}
|
||||
// Test nil store
|
||||
if (&Endpoint{
|
||||
Conditions: []Condition{statusCondition},
|
||||
Store: nil,
|
||||
}).needsToReadBody() {
|
||||
t.Error("expected false when store is nil, got true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_needsToRetrieveDomainExpiration(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
@@ -175,10 +176,12 @@ func StoreResultValues(ctx *gontext.Gontext, mappings map[string]string, result
|
||||
return nil, nil
|
||||
}
|
||||
storedValues := make(map[string]interface{})
|
||||
var extractionErrors []string
|
||||
for contextKey, placeholder := range mappings {
|
||||
value, err := extractValueForStorage(placeholder, result)
|
||||
if err != nil {
|
||||
// Continue storing other values even if one fails
|
||||
extractionErrors = append(extractionErrors, fmt.Sprintf("%s: %v", contextKey, err))
|
||||
storedValues[contextKey] = fmt.Sprintf("ERROR: %v", err)
|
||||
continue
|
||||
}
|
||||
@@ -187,6 +190,10 @@ func StoreResultValues(ctx *gontext.Gontext, mappings map[string]string, result
|
||||
}
|
||||
storedValues[contextKey] = value
|
||||
}
|
||||
// Return an error if any values failed to extract
|
||||
if len(extractionErrors) > 0 {
|
||||
return storedValues, fmt.Errorf("failed to extract values: %s", strings.Join(extractionErrors, "; "))
|
||||
}
|
||||
return storedValues, nil
|
||||
}
|
||||
|
||||
@@ -197,6 +204,11 @@ func extractValueForStorage(placeholder string, result *endpoint.Result) (interf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check if the resolution resulted in an INVALID placeholder
|
||||
// This happens when a path doesn't exist (e.g., [BODY].nonexistent)
|
||||
if strings.HasSuffix(resolved, " "+endpoint.InvalidConditionElementSuffix) {
|
||||
return nil, fmt.Errorf("invalid path: %s", strings.TrimSuffix(resolved, " "+endpoint.InvalidConditionElementSuffix))
|
||||
}
|
||||
// Try to parse as number or boolean to store as proper types
|
||||
// Try int first for whole numbers
|
||||
if num, err := strconv.ParseInt(resolved, 10, 64); err == nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package suite
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -240,6 +241,50 @@ func TestStoreResultValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreResultValuesWithInvalidPath(t *testing.T) {
|
||||
ctx := gontext.New(map[string]interface{}{})
|
||||
result := &endpoint.Result{
|
||||
HTTPStatus: 200,
|
||||
Body: []byte(`{"data": {"name": "john"}}`),
|
||||
}
|
||||
// Define store mappings with invalid paths
|
||||
mappings := map[string]string{
|
||||
"valid_status": "[STATUS]",
|
||||
"invalid_token": "[BODY].accessToken", // This path doesn't exist
|
||||
"invalid_nested": "[BODY].user.id.invalid", // This nested path doesn't exist
|
||||
}
|
||||
// Store values - should return error for invalid paths
|
||||
stored, err := StoreResultValues(ctx, mappings, result)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when storing invalid paths, got nil")
|
||||
}
|
||||
// Check that the error message contains information about the invalid paths
|
||||
if !strings.Contains(err.Error(), "invalid_token") {
|
||||
t.Errorf("Error should mention invalid_token, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid path") {
|
||||
t.Errorf("Error should mention 'invalid path', got: %v", err)
|
||||
}
|
||||
// Verify that valid values were still stored
|
||||
if stored["valid_status"] != int64(200) {
|
||||
t.Errorf("Expected valid_status=200, got %v", stored["valid_status"])
|
||||
}
|
||||
// Verify that invalid values show error messages in stored map
|
||||
if !strings.Contains(stored["invalid_token"].(string), "ERROR") {
|
||||
t.Errorf("Expected invalid_token to contain ERROR, got %v", stored["invalid_token"])
|
||||
}
|
||||
// Verify that invalid values are NOT in context
|
||||
_, err = ctx.Get("invalid_token")
|
||||
if err == nil {
|
||||
t.Error("Invalid token should not be stored in context")
|
||||
}
|
||||
// Verify that valid value IS in context
|
||||
val, err := ctx.Get("valid_status")
|
||||
if err != nil || val != int64(200) {
|
||||
t.Errorf("Expected valid_status=200 in context, got %v, err=%v", val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuite_ExecuteWithAlwaysRunEndpoints(t *testing.T) {
|
||||
suite := &Suite{
|
||||
Name: "test-suite",
|
||||
|
||||
@@ -289,6 +289,7 @@ func (s *Store) InsertEndpointResult(ep *endpoint.Endpoint, result *endpoint.Res
|
||||
// Get the success value of the previous result
|
||||
var lastResultSuccess bool
|
||||
if lastResultSuccess, err = s.getLastEndpointResultSuccessValue(tx, endpointID); err != nil {
|
||||
// Silently fail
|
||||
logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve outcome of previous result for endpoint with key=%s: %s", ep.Key(), err.Error())
|
||||
} else {
|
||||
// If we managed to retrieve the outcome of the previous result, we'll compare it with the new result.
|
||||
@@ -758,13 +759,19 @@ func (s *Store) getEndpointIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64
|
||||
}
|
||||
|
||||
func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (events []*endpoint.Event, err error) {
|
||||
// We need to get the most recent events, but return them in chronological order (oldest to newest)
|
||||
// First, get the most recent events using a subquery, then order them chronologically
|
||||
rows, err := tx.Query(
|
||||
`
|
||||
SELECT event_type, event_timestamp
|
||||
FROM endpoint_events
|
||||
WHERE endpoint_id = $1
|
||||
FROM (
|
||||
SELECT event_type, event_timestamp, endpoint_event_id
|
||||
FROM endpoint_events
|
||||
WHERE endpoint_id = $1
|
||||
ORDER BY endpoint_event_id DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
) AS recent_events
|
||||
ORDER BY endpoint_event_id ASC
|
||||
LIMIT $2 OFFSET $3
|
||||
`,
|
||||
endpointID,
|
||||
pageSize,
|
||||
|
||||
@@ -886,3 +886,59 @@ func TestStore_HasEndpointStatusNewerThan(t *testing.T) {
|
||||
t.Error("expected not to have a newer status in the future")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEventOrderingFix specifically tests the SQL ordering fix for issue #1040
|
||||
// This test verifies that getEndpointEventsByEndpointID returns the most recent events
|
||||
// in chronological order (oldest to newest)
|
||||
func TestEventOrderingFix(t *testing.T) {
|
||||
store, _ := NewStore("sqlite", t.TempDir()+"/test.db", false, 100, 100)
|
||||
defer store.Close()
|
||||
ep := &endpoint.Endpoint{
|
||||
Name: "ordering-test",
|
||||
Group: "test",
|
||||
URL: "https://example.com",
|
||||
}
|
||||
// Create many events over time
|
||||
baseTime := time.Now().Add(-100 * time.Hour) // Start 100 hours ago
|
||||
for i := 0; i < 50; i++ {
|
||||
result := &endpoint.Result{
|
||||
Success: i%2 == 0, // Alternate between true/false to create events
|
||||
Timestamp: baseTime.Add(time.Duration(i) * time.Hour),
|
||||
}
|
||||
err := store.InsertEndpointResult(ep, result)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert result %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
// Now retrieve events with pagination to test the ordering
|
||||
tx, _ := store.db.Begin()
|
||||
endpointID, _, _, _ := store.getEndpointIDGroupAndNameByKey(tx, ep.Key())
|
||||
// Get the first page (should get the MOST RECENT events, but in chronological order)
|
||||
events, err := store.getEndpointEventsByEndpointID(tx, endpointID, 1, 10)
|
||||
tx.Commit()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get events: %v", err)
|
||||
}
|
||||
if len(events) != 10 {
|
||||
t.Errorf("Expected 10 events, got %d", len(events))
|
||||
}
|
||||
// Verify the events are in chronological order (oldest to newest)
|
||||
for i := 1; i < len(events); i++ {
|
||||
if events[i].Timestamp.Before(events[i-1].Timestamp) {
|
||||
t.Errorf("Events not in chronological order: event %d timestamp %v is before event %d timestamp %v",
|
||||
i, events[i].Timestamp, i-1, events[i-1].Timestamp)
|
||||
}
|
||||
}
|
||||
// Verify these are the most recent events
|
||||
// The last event in the returned list should be close to "now" (within the last few events we created)
|
||||
lastEventTime := events[len(events)-1].Timestamp
|
||||
expectedRecentTime := baseTime.Add(49 * time.Hour) // The most recent event we created
|
||||
timeDiff := expectedRecentTime.Sub(lastEventTime)
|
||||
if timeDiff > 10*time.Hour { // Allow some margin for events
|
||||
t.Errorf("Events are not the most recent ones. Last event time: %v, expected around: %v (diff: %v)",
|
||||
lastEventTime, expectedRecentTime, timeDiff)
|
||||
}
|
||||
t.Logf("Successfully retrieved %d most recent events in chronological order", len(events))
|
||||
t.Logf("First event: %s at %v", events[0].Type, events[0].Timestamp)
|
||||
t.Logf("Last event: %s at %v", events[len(events)-1].Type, events[len(events)-1].Timestamp)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user