From cd10b31ab570d083b16774906504e0b45477ecfd Mon Sep 17 00:00:00 2001 From: TwiN Date: Sat, 20 Sep 2025 19:28:27 -0400 Subject: [PATCH] fix(condition): Properly format conditions with invalid context placeholders (#1281) --- config/endpoint/condition.go | 41 +++++++++-------- config/endpoint/condition_test.go | 76 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/config/endpoint/condition.go b/config/endpoint/condition.go index 184b5085..ee40080a 100644 --- a/config/endpoint/condition.go +++ b/config/endpoint/condition.go @@ -214,30 +214,35 @@ func prettifyNumericalParameters(parameters []string, resolvedParameters []int64 // prettify returns a string representation of a condition with its parameters resolved between parentheses func prettify(parameters []string, resolvedParameters []string, operator string) string { - // Since, in the event of an invalid path, the resolvedParameters also contain the condition itself, - // we'll return the resolvedParameters as-is. - if strings.HasSuffix(resolvedParameters[0], InvalidConditionElementSuffix) || strings.HasSuffix(resolvedParameters[1], InvalidConditionElementSuffix) { - return resolvedParameters[0] + " " + operator + " " + resolvedParameters[1] - } - // If using the pattern function, truncate the parameter it's being compared to if said parameter is long enough + // Handle pattern function truncation first if strings.HasPrefix(parameters[0], PatternFunctionPrefix) && strings.HasSuffix(parameters[0], FunctionSuffix) && len(resolvedParameters[1]) > maximumLengthBeforeTruncatingWhenComparedWithPattern { resolvedParameters[1] = fmt.Sprintf("%.25s...(truncated)", resolvedParameters[1]) } if strings.HasPrefix(parameters[1], PatternFunctionPrefix) && strings.HasSuffix(parameters[1], FunctionSuffix) && len(resolvedParameters[0]) > maximumLengthBeforeTruncatingWhenComparedWithPattern { resolvedParameters[0] = fmt.Sprintf("%.25s...(truncated)", resolvedParameters[0]) } - // First element is a placeholder - if parameters[0] != resolvedParameters[0] && parameters[1] == resolvedParameters[1] { - return parameters[0] + " (" + resolvedParameters[0] + ") " + operator + " " + parameters[1] + // Determine the state of each parameter + leftChanged := parameters[0] != resolvedParameters[0] + rightChanged := parameters[1] != resolvedParameters[1] + leftInvalid := resolvedParameters[0] == parameters[0]+" "+InvalidConditionElementSuffix + rightInvalid := resolvedParameters[1] == parameters[1]+" "+InvalidConditionElementSuffix + // Build the output based on what was resolved + var left, right string + // Format left side + if leftChanged && !leftInvalid { + left = parameters[0] + " (" + resolvedParameters[0] + ")" + } else if leftInvalid { + left = resolvedParameters[0] // Already has (INVALID) + } else { + left = parameters[0] // Unchanged } - // Second element is a placeholder - if parameters[0] == resolvedParameters[0] && parameters[1] != resolvedParameters[1] { - return parameters[0] + " " + operator + " " + parameters[1] + " (" + resolvedParameters[1] + ")" + // Format right side + if rightChanged && !rightInvalid { + right = parameters[1] + " (" + resolvedParameters[1] + ")" + } else if rightInvalid { + right = resolvedParameters[1] // Already has (INVALID) + } else { + right = parameters[1] // Unchanged } - // Both elements are placeholders...? - if parameters[0] != resolvedParameters[0] && parameters[1] != resolvedParameters[1] { - return parameters[0] + " (" + resolvedParameters[0] + ") " + operator + " " + parameters[1] + " (" + resolvedParameters[1] + ")" - } - // Neither elements are placeholders - return parameters[0] + " " + operator + " " + parameters[1] + return left + " " + operator + " " + right } diff --git a/config/endpoint/condition_test.go b/config/endpoint/condition_test.go index 52e68139..aba08010 100644 --- a/config/endpoint/condition_test.go +++ b/config/endpoint/condition_test.go @@ -6,6 +6,8 @@ import ( "strconv" "testing" "time" + + "github.com/TwiN/gatus/v5/config/gontext" ) func TestCondition_Validate(t *testing.T) { @@ -777,3 +779,77 @@ func TestCondition_evaluateWithInvalidOperator(t *testing.T) { t.Error("condition was invalid, result should've had an error") } } + +func TestConditionEvaluateWithInvalidContextPlaceholder(t *testing.T) { + // Test case: Suite endpoint with invalid context placeholder + // This should display the original placeholder names with resolved values + condition := Condition("[STATUS] == [CONTEXT].expected_statusz") + result := &Result{HTTPStatus: 200} + ctx := gontext.New(map[string]interface{}{ + // Note: expected_statusz is not in the context (typo - should be expected_status) + "expected_status": 200, + "max_response_time": 5000, + }) + // Simulate suite endpoint evaluation with context + success := condition.evaluate(result, false, ctx) // false = don't skip resolution (default) + if success { + t.Error("Condition should have failed because [CONTEXT].expected_statusz doesn't exist") + } + if len(result.ConditionResults) == 0 { + t.Fatal("No condition results found") + } + actualDisplay := result.ConditionResults[0].Condition + // The expected format should preserve the placeholder names + expectedDisplay := "[STATUS] (200) == [CONTEXT].expected_statusz (INVALID)" + if actualDisplay != expectedDisplay { + t.Errorf("Incorrect condition display for failed context placeholder\nExpected: %s\nActual: %s", expectedDisplay, actualDisplay) + } +} + +func TestConditionEvaluateWithValidContextPlaceholder(t *testing.T) { + // Test case: Suite endpoint with valid context placeholder + condition := Condition("[STATUS] == [CONTEXT].expected_status") + result := &Result{HTTPStatus: 200} + ctx := gontext.New(map[string]interface{}{ + "expected_status": 200, + }) + // Simulate suite endpoint evaluation with context + success := condition.evaluate(result, false, ctx) + if !success { + t.Error("Condition should have succeeded") + } + if len(result.ConditionResults) == 0 { + t.Fatal("No condition results found") + } + actualDisplay := result.ConditionResults[0].Condition + // For successful conditions, just the original condition is shown + expectedDisplay := "[STATUS] == [CONTEXT].expected_status" + if actualDisplay != expectedDisplay { + t.Errorf("Incorrect condition display for successful context placeholder\nExpected: %s\nActual: %s", expectedDisplay, actualDisplay) + } +} + +func TestConditionEvaluateWithMixedValidAndInvalidContext(t *testing.T) { + // Test case: One valid placeholder, one invalid + // Note: For numerical comparisons, invalid placeholders that can't be parsed as numbers + // default to 0 due to sanitizeAndResolveNumericalWithContext's behavior + condition := Condition("[RESPONSE_TIME] < [CONTEXT].invalid_key") + result := &Result{Duration: 100 * 1000000} // 100ms in nanoseconds + ctx := gontext.New(map[string]interface{}{ + "valid_key": 5000, + }) + // Simulate suite endpoint evaluation with context + success := condition.evaluate(result, false, ctx) + if success { + t.Error("Condition should have failed because [CONTEXT].invalid_key doesn't exist") + } + if len(result.ConditionResults) == 0 { + t.Fatal("No condition results found") + } + actualDisplay := result.ConditionResults[0].Condition + // For numerical comparisons, invalid context placeholders become 0 + expectedDisplay := "[RESPONSE_TIME] (100) < [CONTEXT].invalid_key (0)" + if actualDisplay != expectedDisplay { + t.Errorf("Incorrect condition display\nExpected: %s\nActual: %s", expectedDisplay, actualDisplay) + } +}