Compare commits

..

18 Commits

Author SHA1 Message Date
TwinProduction
2207dd9c32 Fix test 2021-01-21 16:34:40 -05:00
TwinProduction
3204a79eb6 Lazily retry triggered alerts in case of failure 2021-01-21 16:14:32 -05:00
TwinProduction
e9ac115a95 Replace ✔️ by 2021-01-20 17:47:21 -05:00
TwinProduction
c90c786f39 Tweak build action 2021-01-19 00:01:55 -05:00
TwinProduction
f10e2ac639 Tweak build action 2021-01-18 23:55:41 -05:00
TwinProduction
c2d899f2a3 Tweak build action 2021-01-18 23:52:48 -05:00
TwinProduction
7415d8e361 Tweak build action 2021-01-18 23:47:17 -05:00
TwinProduction
298dcc4790 Tweak build action 2021-01-18 23:43:02 -05:00
TwinProduction
2f2890c093 Tweak build action 2021-01-18 23:38:23 -05:00
TwinProduction
e463aec5f6 Tweak build action 2021-01-18 23:34:16 -05:00
TwinProduction
6b3e11a47c Minor update 2021-01-18 23:28:33 -05:00
TwinProduction
0985e3bed8 Minor update 2021-01-17 16:38:22 -05:00
TwinProduction
6d8fd267de Fix mattermost docker-compose example 2021-01-16 20:36:59 -05:00
TwinProduction
e89bb932ea Fix pattern issue 2021-01-15 20:11:43 -05:00
TwinProduction
77737dbab6 Add TestCondition_evaluateWithBodyHTMLPattern 2021-01-15 19:45:17 -05:00
TwinProduction
271c3dc91d Performance improvements 2021-01-14 22:49:48 -05:00
TwinProduction
5860a27ab5 Improve existing tests 2021-01-14 22:49:19 -05:00
TwinProduction
819093cb7e Implement any function and prettify displayed condition on failure 2021-01-14 20:08:27 -05:00
14 changed files with 1453 additions and 1062 deletions

View File

@@ -15,16 +15,18 @@ jobs:
timeout-minutes: 5
steps:
- name: Set up Go 1.15
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: 1.15
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build binary to make sure it works
run: go build -mod vendor
- name: Test
run: sudo go test -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic
# We're using "sudo" because one of the tests leverages ping, which requires super-user privileges.
# As for the "PATH=$PATH", we need it to use the same "go" executable that was configured by the "Set
# up Go" step (otherwise, it'd use sudo's "go" executable)
run: sudo "PATH=$PATH" go test -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic
- name: Codecov
uses: codecov/codecov-action@v1.0.14
with:

1485
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package custom
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
@@ -76,6 +77,9 @@ func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription st
// Send a request to the alert provider and return the body
func (provider *AlertProvider) Send(serviceName, alertDescription string, resolved bool) ([]byte, error) {
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
return nil, errors.New("error")
}
return []byte("{}"), nil
}
request := provider.buildHTTPRequest(serviceName, alertDescription, resolved)

View File

@@ -31,7 +31,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = ":heavy_check_mark:"
prefix = ":white_check_mark:"
} else {
prefix = ":x:"
}

View File

@@ -165,7 +165,7 @@ web:
port: 12345
services:
- name: twinnation
url: https://twinnation.org/actuator/health
url: https://twinnation.org/health
conditions:
- "[STATUS] == 200"
`))
@@ -178,17 +178,15 @@ services:
if config.Metrics {
t.Error("Metrics should've been false by default")
}
if config.Services[0].URL != "https://twinnation.org/actuator/health" {
t.Errorf("URL should have been %s", "https://twinnation.org/actuator/health")
if config.Services[0].URL != "https://twinnation.org/health" {
t.Errorf("URL should have been %s", "https://twinnation.org/health")
}
if config.Services[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
}
if config.Web.Address != DefaultAddress {
t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress)
}
if config.Web.Port != 12345 {
t.Errorf("Port should have been %d, because it is specified in config", 12345)
}

View File

@@ -26,6 +26,12 @@ type Alert struct {
// Triggered is used to determine whether an alert has been triggered. When an alert is resolved, this value
// should be set back to false. It is used to prevent the same alert from going out twice.
//
// This value should only be modified if the provider.AlertProvider's Send function does not return an error for an
// alert that hasn't been triggered yet. This doubles as a lazy retry. The reason why this behavior isn't also
// applied for alerts that are already triggered and has become "healthy" again is to prevent a case where, for
// some reason, the alert provider always returns errors when trying to send the resolved notification
// (SendOnResolved).
Triggered bool
}

View File

@@ -2,7 +2,6 @@ package core
import (
"fmt"
"log"
"strconv"
"strings"
"time"
@@ -48,11 +47,20 @@ const (
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
// LengthFunctionPrefix is the prefix for the length function
//
// Usage: len([BODY].articles) == 10, len([BODY].name) > 5
LengthFunctionPrefix = "len("
// PatternFunctionPrefix is the prefix for the pattern function
//
// Usage: pat(192.168.*.*)
PatternFunctionPrefix = "pat("
// AnyFunctionPrefix is the prefix for the any function
//
// Usage: any(1.1.1.1, 1.0.0.1)
AnyFunctionPrefix = "any("
// FunctionSuffix is the suffix for all functions
FunctionSuffix = ")"
@@ -64,51 +72,52 @@ const (
type Condition string
// evaluate the Condition with the Result of the health check
func (c *Condition) evaluate(result *Result) bool {
condition := string(*c)
func (c Condition) evaluate(result *Result) bool {
condition := string(c)
success := false
var resolvedCondition string
conditionToDisplay := condition
if strings.Contains(condition, "==") {
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
success = isEqual(parts[0], parts[1])
resolvedCondition = fmt.Sprintf("%v == %v", parts[0], parts[1])
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "=="), result)
success = isEqual(resolvedParameters[0], resolvedParameters[1])
if !success {
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
}
} else if strings.Contains(condition, "!=") {
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
success = !isEqual(parts[0], parts[1])
resolvedCondition = fmt.Sprintf("%v != %v", parts[0], parts[1])
parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, "!="), result)
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
if !success {
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
}
} else if strings.Contains(condition, "<=") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
success = parts[0] <= parts[1]
resolvedCondition = fmt.Sprintf("%v <= %v", parts[0], parts[1])
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
success = resolvedParameters[0] <= resolvedParameters[1]
if !success {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
}
} else if strings.Contains(condition, ">=") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
success = parts[0] >= parts[1]
resolvedCondition = fmt.Sprintf("%v >= %v", parts[0], parts[1])
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
success = resolvedParameters[0] >= resolvedParameters[1]
if !success {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
}
} else if strings.Contains(condition, ">") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
success = parts[0] > parts[1]
resolvedCondition = fmt.Sprintf("%v > %v", parts[0], parts[1])
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
success = resolvedParameters[0] > resolvedParameters[1]
if !success {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
}
} else if strings.Contains(condition, "<") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
success = parts[0] < parts[1]
resolvedCondition = fmt.Sprintf("%v < %v", parts[0], parts[1])
parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
success = resolvedParameters[0] < resolvedParameters[1]
if !success {
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
}
} else {
result.Errors = append(result.Errors, fmt.Sprintf("invalid condition '%s' has been provided", condition))
return false
}
conditionToDisplay := condition
// If the condition isn't a success, return what the resolved condition was too
if !success {
log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, resolvedCondition)
// Check if the resolved condition was an invalid path
isResolvedConditionInvalidPath := strings.ReplaceAll(resolvedCondition, fmt.Sprintf("%s ", InvalidConditionElementSuffix), "") == condition
if isResolvedConditionInvalidPath {
// Since, in the event of an invalid path, the resolvedCondition contains the condition itself,
// we'll only display the resolvedCondition
conditionToDisplay = resolvedCondition
} else {
conditionToDisplay = fmt.Sprintf("%s (%s)", condition, resolvedCondition)
}
//log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
}
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
return success
@@ -116,33 +125,66 @@ func (c *Condition) evaluate(result *Result) bool {
// isEqual compares two strings.
//
// It also supports the pattern function. That is to say, if one of the strings starts with PatternFunctionPrefix
// and ends with FunctionSuffix, it will be treated like a pattern.
// Supports the pattern and the any functions.
// i.e. if one of the parameters starts with PatternFunctionPrefix and ends with FunctionSuffix, it will be treated like
// a pattern.
func isEqual(first, second string) bool {
var isFirstPattern, isSecondPattern bool
if strings.HasPrefix(first, PatternFunctionPrefix) && strings.HasSuffix(first, FunctionSuffix) {
isFirstPattern = true
first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix)
}
if strings.HasPrefix(second, PatternFunctionPrefix) && strings.HasSuffix(second, FunctionSuffix) {
isSecondPattern = true
second = strings.TrimSuffix(strings.TrimPrefix(second, PatternFunctionPrefix), FunctionSuffix)
}
if isFirstPattern && !isSecondPattern {
return pattern.Match(first, second)
} else if !isFirstPattern && isSecondPattern {
return pattern.Match(second, first)
} else {
return first == second
firstHasFunctionSuffix := strings.HasSuffix(first, FunctionSuffix)
secondHasFunctionSuffix := strings.HasSuffix(second, FunctionSuffix)
if firstHasFunctionSuffix || secondHasFunctionSuffix {
var isFirstPattern, isSecondPattern bool
if strings.HasPrefix(first, PatternFunctionPrefix) && firstHasFunctionSuffix {
isFirstPattern = true
first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix)
}
if strings.HasPrefix(second, PatternFunctionPrefix) && secondHasFunctionSuffix {
isSecondPattern = true
second = strings.TrimSuffix(strings.TrimPrefix(second, PatternFunctionPrefix), FunctionSuffix)
}
if isFirstPattern && !isSecondPattern {
return pattern.Match(first, second)
} else if !isFirstPattern && isSecondPattern {
return pattern.Match(second, first)
}
var isFirstAny, isSecondAny bool
if strings.HasPrefix(first, AnyFunctionPrefix) && firstHasFunctionSuffix {
isFirstAny = true
first = strings.TrimSuffix(strings.TrimPrefix(first, AnyFunctionPrefix), FunctionSuffix)
}
if strings.HasPrefix(second, AnyFunctionPrefix) && secondHasFunctionSuffix {
isSecondAny = true
second = strings.TrimSuffix(strings.TrimPrefix(second, AnyFunctionPrefix), FunctionSuffix)
}
if isFirstAny && !isSecondAny {
options := strings.Split(first, ",")
for _, option := range options {
if strings.TrimSpace(option) == second {
return true
}
}
return false
} else if !isFirstAny && isSecondAny {
options := strings.Split(second, ",")
for _, option := range options {
if strings.TrimSpace(option) == first {
return true
}
}
return false
}
}
return first == second
}
// sanitizeAndResolve sanitizes and resolves a list of element and returns the list of resolved elements
func sanitizeAndResolve(list []string, result *Result) []string {
var sanitizedList []string
// sanitizeAndResolve sanitizes and resolves a list of elements and returns the list of parameters as well as a list
// of resolved parameters
func sanitizeAndResolve(elements []string, result *Result) ([]string, []string) {
parameters := make([]string, len(elements))
resolvedParameters := make([]string, len(elements))
body := strings.TrimSpace(string(result.Body))
for _, element := range list {
for i, element := range elements {
element = strings.TrimSpace(element)
parameters[i] = element
switch strings.ToUpper(element) {
case StatusPlaceholder:
element = strconv.Itoa(result.HTTPStatus)
@@ -166,42 +208,68 @@ func sanitizeAndResolve(list []string, result *Result) []string {
wantLength = true
element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix)
}
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceholder), "", 1), result.Body)
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.TrimPrefix(element, BodyPlaceholder+"."), result.Body)
if err != nil {
if err.Error() != "unexpected end of JSON input" {
result.Errors = append(result.Errors, err.Error())
}
if wantLength {
element = fmt.Sprintf("%s%s%s %s", LengthFunctionPrefix, element, FunctionSuffix, InvalidConditionElementSuffix)
element = LengthFunctionPrefix + element + FunctionSuffix + " " + InvalidConditionElementSuffix
} else {
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
element = element + " " + InvalidConditionElementSuffix
}
} else {
if wantLength {
element = fmt.Sprintf("%d", resolvedElementLength)
element = strconv.Itoa(resolvedElementLength)
} else {
element = resolvedElement
}
}
}
}
sanitizedList = append(sanitizedList, element)
resolvedParameters[i] = element
}
return sanitizedList
return parameters, resolvedParameters
}
func sanitizeAndResolveNumerical(list []string, result *Result) []int64 {
var sanitizedNumbers []int64
sanitizedList := sanitizeAndResolve(list, result)
for _, element := range sanitizedList {
func sanitizeAndResolveNumerical(list []string, result *Result) (parameters []string, resolvedNumericalParameters []int64) {
parameters, resolvedParameters := sanitizeAndResolve(list, result)
for _, element := range resolvedParameters {
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds())
resolvedNumericalParameters = append(resolvedNumericalParameters, duration.Milliseconds())
} else if number, err := strconv.ParseInt(element, 10, 64); err != nil {
// Default to 0 if the string couldn't be converted to an integer
sanitizedNumbers = append(sanitizedNumbers, 0)
resolvedNumericalParameters = append(resolvedNumericalParameters, 0)
} else {
sanitizedNumbers = append(sanitizedNumbers, number)
resolvedNumericalParameters = append(resolvedNumericalParameters, number)
}
}
return sanitizedNumbers
return parameters, resolvedNumericalParameters
}
func prettifyNumericalParameters(parameters []string, resolvedParameters []int64, operator string) string {
return prettify(parameters, []string{strconv.Itoa(int(resolvedParameters[0])), strconv.Itoa(int(resolvedParameters[1]))}, operator)
}
// XXX: make this configurable? i.e. show-resolved-conditions-on-failure
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]
}
// First element is a placeholder
if parameters[0] != resolvedParameters[0] && parameters[1] == resolvedParameters[1] {
return parameters[0] + " (" + resolvedParameters[0] + ") " + operator + " " + parameters[1]
}
// Second element is a placeholder
if parameters[0] == resolvedParameters[0] && parameters[1] != resolvedParameters[1] {
return parameters[0] + " " + operator + " " + parameters[1] + " (" + resolvedParameters[1] + ")"
}
// 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]
}

View File

@@ -0,0 +1,75 @@
package core
import "testing"
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
for n := 0; n < b.N; n++ {
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithBodyStringAnyFailure(b *testing.B) {
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
for n := 0; n < b.N; n++ {
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithBodyString(b *testing.B) {
condition := Condition("[BODY].name == john.doe")
for n := 0; n < b.N; n++ {
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithBodyStringFailure(b *testing.B) {
condition := Condition("[BODY].name == john.doe")
for n := 0; n < b.N; n++ {
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithBodyStringLen(b *testing.B) {
condition := Condition("len([BODY].name) == 8")
for n := 0; n < b.N; n++ {
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithBodyStringLenFailure(b *testing.B) {
condition := Condition("len([BODY].name) == 8")
for n := 0; n < b.N; n++ {
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithStatus(b *testing.B) {
condition := Condition("[STATUS] == 200")
for n := 0; n < b.N; n++ {
result := &Result{HTTPStatus: 200}
condition.evaluate(result)
}
b.ReportAllocs()
}
func BenchmarkCondition_evaluateWithStatusFailure(b *testing.B) {
condition := Condition("[STATUS] == 200")
for n := 0; n < b.N; n++ {
result := &Result{HTTPStatus: 400}
condition.evaluate(result)
}
b.ReportAllocs()
}

View File

@@ -1,6 +1,7 @@
package core
import (
"fmt"
"strconv"
"testing"
"time"
@@ -22,6 +23,9 @@ func TestCondition_evaluateWithStatus(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
if result.ConditionResults[0].Condition != string(condition) {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, condition, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithStatusFailure(t *testing.T) {
@@ -31,6 +35,10 @@ func TestCondition_evaluateWithStatusFailure(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[STATUS] (500) == 200"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithStatusUsingLessThan(t *testing.T) {
@@ -40,6 +48,10 @@ func TestCondition_evaluateWithStatusUsingLessThan(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[STATUS] < 300"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithStatusFailureUsingLessThan(t *testing.T) {
@@ -49,6 +61,10 @@ func TestCondition_evaluateWithStatusFailureUsingLessThan(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[STATUS] (404) < 300"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) {
@@ -58,6 +74,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] < 500"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) {
@@ -67,6 +87,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] < 1s"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) {
@@ -76,6 +100,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have failed because the condition has an invalid numerical value that should've automatically resolved to 0", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] (50) < potato (0)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
@@ -87,6 +115,10 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] > 500"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T) {
@@ -96,6 +128,10 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] > 1s"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
@@ -105,6 +141,10 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] >= 500"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
@@ -114,6 +154,10 @@ func TestCondition_evaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[RESPONSE_TIME] <= 500"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBody(t *testing.T) {
@@ -123,6 +167,10 @@ func TestCondition_evaluateWithBody(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY] == test"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPath(t *testing.T) {
@@ -132,6 +180,10 @@ func TestCondition_evaluateWithBodyJSONPath(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].status == UP"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathComplex(t *testing.T) {
@@ -141,31 +193,35 @@ func TestCondition_evaluateWithBodyJSONPathComplex(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].data.name == john"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithInvalidBodyJSONPathComplex(t *testing.T) {
expectedResolvedCondition := "[BODY].data.name (INVALID) == john"
condition := Condition("[BODY].data.name == john")
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure, because the path was invalid", condition)
}
if result.ConditionResults[0].Condition != expectedResolvedCondition {
t.Errorf("Condition '%s' should have resolved to '%s', but resolved to '%s' instead", condition, expectedResolvedCondition, result.ConditionResults[0].Condition)
expectedConditionDisplayed := "[BODY].data.name (INVALID) == john"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithInvalidBodyJSONPathComplexWithLengthFunction(t *testing.T) {
expectedResolvedCondition := "len([BODY].data.name) (INVALID) == john"
condition := Condition("len([BODY].data.name) == john")
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure, because the path was invalid", condition)
}
if result.ConditionResults[0].Condition != expectedResolvedCondition {
t.Errorf("Condition '%s' should have resolved to '%s', but resolved to '%s' instead", condition, expectedResolvedCondition, result.ConditionResults[0].Condition)
expectedConditionDisplayed := "len([BODY].data.name) (INVALID) == john"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
@@ -176,6 +232,10 @@ func TestCondition_evaluateWithBodyJSONPathDoublePlaceholders(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].user.firstName != [BODY].user.lastName"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathDoublePlaceholdersFailure(t *testing.T) {
@@ -185,6 +245,10 @@ func TestCondition_evaluateWithBodyJSONPathDoublePlaceholdersFailure(t *testing.
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[BODY].user.firstName (john) == [BODY].user.lastName (doe)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathLongInt(t *testing.T) {
@@ -194,6 +258,10 @@ func TestCondition_evaluateWithBodyJSONPathLongInt(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].data.id == 1"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathComplexInt(t *testing.T) {
@@ -203,6 +271,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexInt(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].data[1].id == 2"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathComplexIntUsingGreaterThan(t *testing.T) {
@@ -212,6 +284,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntUsingGreaterThan(t *testing
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].data.id > 0"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingGreaterThan(t *testing.T) {
@@ -221,6 +297,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingGreaterThan(t *
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[BODY].data.id (1) > 5"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathComplexIntUsingLessThan(t *testing.T) {
@@ -230,6 +310,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntUsingLessThan(t *testing.T)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].data.id < 5"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingLessThan(t *testing.T) {
@@ -239,6 +323,10 @@ func TestCondition_evaluateWithBodyJSONPathComplexIntFailureUsingLessThan(t *tes
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[BODY].data.id (10) < 5"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodySliceLength(t *testing.T) {
@@ -248,6 +336,10 @@ func TestCondition_evaluateWithBodySliceLength(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "len([BODY].data) == 3"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyStringLength(t *testing.T) {
@@ -257,6 +349,36 @@ func TestCondition_evaluateWithBodyStringLength(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "len([BODY].name) == 8"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyPattern(t *testing.T) {
condition := Condition("[BODY] == pat(*john*)")
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY] == pat(*john*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithReverseBodyPattern(t *testing.T) {
condition := Condition("pat(*john*) == [BODY]")
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "pat(*john*) == [BODY]"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyStringPattern(t *testing.T) {
@@ -266,6 +388,24 @@ func TestCondition_evaluateWithBodyStringPattern(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY].name == pat(*ohn*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyHTMLPattern(t *testing.T) {
var html = `<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body><div id="user">john.doe</div></body></html>`
condition := Condition("[BODY] == pat(*<div id=\"user\">john.doe</div>*)")
result := &Result{Body: []byte(html)}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[BODY] == pat(*<div id=\"user\">john.doe</div>*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyStringPatternFailure(t *testing.T) {
@@ -275,14 +415,9 @@ func TestCondition_evaluateWithBodyStringPatternFailure(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
}
func TestCondition_evaluateWithBodyPatternFailure(t *testing.T) {
condition := Condition("[BODY] == pat(*john*)")
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
expectedConditionDisplayed := "[BODY].name (john.doe) == pat(bob*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
@@ -293,6 +428,10 @@ func TestCondition_evaluateWithIPPattern(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[IP] == pat(10.*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithIPPatternFailure(t *testing.T) {
@@ -302,6 +441,10 @@ func TestCondition_evaluateWithIPPatternFailure(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[IP] (255.255.255.255) == pat(10.*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithStatusPattern(t *testing.T) {
@@ -311,6 +454,10 @@ func TestCondition_evaluateWithStatusPattern(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[STATUS] == pat(4*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithStatusPatternFailure(t *testing.T) {
@@ -320,6 +467,105 @@ func TestCondition_evaluateWithStatusPatternFailure(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[STATUS] (404) != pat(4*)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithBodyStringAny(t *testing.T) {
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
expectedConditionDisplayed := "[BODY].name == any(john.doe, jane.doe)"
results := []*Result{
{Body: []byte("{\"name\": \"john.doe\"}")},
{Body: []byte("{\"name\": \"jane.doe\"}")},
}
for _, result := range results {
success := condition.evaluate(result)
if !success || !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
}
func TestCondition_evaluateWithBodyStringAnyFailure(t *testing.T) {
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[BODY].name (bob.doe) == any(john.doe, jane.doe)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithStatusAny(t *testing.T) {
condition := Condition("[STATUS] == any(200, 429)")
statuses := []int{200, 429}
for _, status := range statuses {
result := &Result{HTTPStatus: status}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[STATUS] == any(200, 429)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
}
func TestCondition_evaluateWithReverseStatusAny(t *testing.T) {
condition := Condition("any(200, 429) == [STATUS]")
statuses := []int{200, 429}
for _, status := range statuses {
result := &Result{HTTPStatus: status}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "any(200, 429) == [STATUS]"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
}
func TestCondition_evaluateWithStatusAnyFailure(t *testing.T) {
condition := Condition("[STATUS] == any(200, 429)")
statuses := []int{201, 400, 404, 500}
for _, status := range statuses {
result := &Result{HTTPStatus: status}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := fmt.Sprintf("[STATUS] (%d) == any(200, 429)", status)
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
}
func TestCondition_evaluateWithReverseStatusAnyFailure(t *testing.T) {
condition := Condition("any(200, 429) == [STATUS]")
statuses := []int{201, 400, 404, 500}
for _, status := range statuses {
result := &Result{HTTPStatus: status}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := fmt.Sprintf("any(200, 429) == [STATUS] (%d)", status)
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
}
func TestCondition_evaluateWithConnected(t *testing.T) {
@@ -329,6 +575,10 @@ func TestCondition_evaluateWithConnected(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[CONNECTED] == true"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithConnectedFailure(t *testing.T) {
@@ -338,6 +588,10 @@ func TestCondition_evaluateWithConnectedFailure(t *testing.T) {
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[CONNECTED] (false) == true"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) {
@@ -347,6 +601,10 @@ func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) {
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] == 0"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *testing.T) {
@@ -357,6 +615,10 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *test
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] > 2419200000"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(t *testing.T) {
@@ -367,6 +629,10 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] (1209600000) > 2419200000"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testing.T) {
@@ -376,6 +642,10 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testi
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] > 12h"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t *testing.T) {
@@ -385,4 +655,8 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
expectedConditionDisplayed := "[CERTIFICATE_EXPIRATION] (86400000) > 48h (172800000)"
if result.ConditionResults[0].Condition != expectedConditionDisplayed {
t.Errorf("Condition '%s' should have resolved to '%s', got '%s'", condition, expectedConditionDisplayed, result.ConditionResults[0].Condition)
}
}

View File

@@ -1,6 +1,16 @@
alerting:
mattermost:
webhook-url: "http://mattermost:8065/hooks/tokengoeshere"
insecure: true
services:
- name: example
url: http://example.org
interval: 30s
interval: 1m
alerts:
- type: mattermost
enabled: true
description: "healthcheck failed 3 times in a row"
send-on-resolved: true
conditions:
- "[STATUS] == 200"

View File

@@ -1,13 +1,24 @@
version: "3.8"
services:
gatus:
image: twinproduction/gatus:latest
ports:
- 8080:8080
volumes:
- ./config.yaml:/config/config.yaml
mattermost-preview:
image: mattermost/mattermost-preview:latest
ports:
- 8065:8065
services:
gatus:
container_name: gatus
image: twinproduction/gatus:latest
ports:
- 8080:8080
volumes:
- ./config.yaml:/config/config.yaml
networks:
- default
mattermost:
container_name: mattermost
image: mattermost/mattermost-preview:5.26.0
ports:
- 8065:8065
networks:
- default
networks:
default:
driver: bridge

View File

@@ -10,10 +10,11 @@ func Match(pattern, s string) bool {
if pattern == "*" {
return true
}
// Backslashes break filepath.Match, so we'll remove all of them.
// This has a pretty significant impact on performance when there
// are backslashes, but at least it doesn't break filepath.Match.
s = strings.ReplaceAll(s, "\\", "")
// Separators found in the string break filepath.Match, so we'll remove all of them.
// This has a pretty significant impact on performance when there are separators in
// the strings, but at least it doesn't break filepath.Match.
s = strings.ReplaceAll(s, string(filepath.Separator), "")
pattern = strings.ReplaceAll(pattern, string(filepath.Separator), "")
matched, _ := filepath.Match(pattern, s)
return matched
}

View File

@@ -26,18 +26,18 @@ func handleAlertsToTrigger(service *core.Service, result *core.Result, cfg *conf
service.NumberOfFailuresInARow++
for _, alert := range service.Alerts {
// If the alert hasn't been triggered, move to the next one
if !alert.Enabled || alert.FailureThreshold != service.NumberOfFailuresInARow {
if !alert.Enabled || alert.FailureThreshold > service.NumberOfFailuresInARow {
continue
}
if alert.Triggered {
if cfg.Debug {
log.Printf("[watchdog][handleAlertsToTrigger] Alert with description='%s' has already been TRIGGERED, skipping", alert.Description)
log.Printf("[watchdog][handleAlertsToTrigger] Alert for service=%s with description='%s' has already been TRIGGERED, skipping", service.Name, alert.Description)
}
continue
}
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
if alertProvider != nil && alertProvider.IsValid() {
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s alert because alert with description='%s' has been TRIGGERED", alert.Type, alert.Description)
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s alert because alert for service=%s with description='%s' has been TRIGGERED", alert.Type, service.Name, alert.Description)
customAlertProvider := alertProvider.ToCustomAlertProvider(service, alert, result, false)
// TODO: retry on error
var err error
@@ -47,7 +47,7 @@ func handleAlertsToTrigger(service *core.Service, result *core.Result, cfg *conf
if body, err = customAlertProvider.Send(service.Name, alert.Description, false); err == nil {
var response pagerDutyResponse
if err = json.Unmarshal(body, &response); err != nil {
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pager duty response: %s", err.Error())
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pagerduty response: %s", err.Error())
} else {
alert.ResolveKey = response.DedupKey
}
@@ -57,7 +57,7 @@ func handleAlertsToTrigger(service *core.Service, result *core.Result, cfg *conf
_, err = customAlertProvider.Send(service.Name, alert.Description, false)
}
if err != nil {
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error sending an alert: %s", err.Error())
log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an alert for service=%s: %s", service.Name, err.Error())
} else {
alert.Triggered = true
}
@@ -73,18 +73,20 @@ func handleAlertsToResolve(service *core.Service, result *core.Result, cfg *conf
if !alert.Enabled || !alert.Triggered || alert.SuccessThreshold > service.NumberOfSuccessesInARow {
continue
}
// Even if the alert provider returns an error, we still set the alert's Triggered variable to false.
// Further explanation can be found on Alert's Triggered field.
alert.Triggered = false
if !alert.SendOnResolved {
continue
}
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
if alertProvider != nil && alertProvider.IsValid() {
log.Printf("[watchdog][handleAlertsToResolve] Sending %s alert because alert with description='%s' has been RESOLVED", alert.Type, alert.Description)
log.Printf("[watchdog][handleAlertsToResolve] Sending %s alert because alert for service=%s with description='%s' has been RESOLVED", alert.Type, service.Name, alert.Description)
customAlertProvider := alertProvider.ToCustomAlertProvider(service, alert, result, true)
// TODO: retry on error
_, err := customAlertProvider.Send(service.Name, alert.Description, true)
if err != nil {
log.Printf("[watchdog][handleAlertsToResolve] Ran into error sending an alert: %s", err.Error())
log.Printf("[watchdog][handleAlertsToResolve] Failed to send an alert for service=%s: %s", service.Name, err.Error())
} else {
if alert.Type == core.PagerDutyAlert {
alert.ResolveKey = ""

View File

@@ -39,103 +39,23 @@ func TestHandleAlerting(t *testing.T) {
},
}
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've started at 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've started at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't start triggered")
}
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 1 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 0 to 1, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't have triggered")
}
verify(t, service, 1, 0, false, "The alert shouldn't have triggered")
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 2 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 1 to 2, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert should've triggered")
}
verify(t, service, 2, 0, true, "The alert should've triggered")
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 3 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 2 to 3, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert should still show as triggered")
}
verify(t, service, 3, 0, true, "The alert should still be triggered")
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 4 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 3 to 4, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert should still show as triggered")
}
verify(t, service, 4, 0, true, "The alert should still be triggered")
HandleAlerting(service, &core.Result{Success: true})
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've reset to 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 1 {
t.Fatal("service.NumberOfSuccessesInARow should've increased from 0 to 1, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
}
verify(t, service, 0, 1, true, "The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
HandleAlerting(service, &core.Result{Success: true})
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've stayed at 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 2 {
t.Fatal("service.NumberOfSuccessesInARow should've increased from 1 to 2, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert should still be triggered")
}
verify(t, service, 0, 2, true, "The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
HandleAlerting(service, &core.Result{Success: true})
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've stayed at 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 3 {
t.Fatal("service.NumberOfSuccessesInARow should've increased from 2 to 3, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert should not be triggered")
}
verify(t, service, 0, 3, false, "The alert should've been resolved")
HandleAlerting(service, &core.Result{Success: true})
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've stayed at 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 4 {
t.Fatal("service.NumberOfSuccessesInARow should've increased from 3 to 4, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert should no longer be triggered")
}
verify(t, service, 0, 4, false, "The alert should no longer be triggered")
}
func TestHandleAlertingWhenAlertingConfigIsNil(t *testing.T) {
@@ -172,92 +92,11 @@ func TestHandleAlertingWithBadAlertProvider(t *testing.T) {
},
}
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've started at 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've started at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't start triggered")
}
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 1 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 0 to 1, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't have triggered")
}
verify(t, service, 1, 0, false, "The alert shouldn't have triggered")
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 2 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 1 to 2, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't have triggered, because the provider wasn't configured properly")
}
}
func TestHandleAlertingWithoutSendingAlertOnResolve(t *testing.T) {
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
defer os.Clearenv()
cfg := &config.Config{
Alerting: &alerting.Config{},
}
config.Set(cfg)
service := &core.Service{
URL: "http://example.com",
Alerts: []*core.Alert{
{
Type: core.CustomAlert,
Enabled: true,
FailureThreshold: 1,
SuccessThreshold: 1,
SendOnResolved: false,
Triggered: false,
},
},
}
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've started at 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've started at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't start triggered")
}
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 1 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 0 to 1, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't have triggered")
}
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 2 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 1 to 2, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't have triggered, because the provider wasn't configured properly")
}
verify(t, service, 2, 0, false, "The alert shouldn't have triggered, because the provider wasn't configured properly")
}
func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingAgain(t *testing.T) {
@@ -291,15 +130,7 @@ func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingA
// This test simulate an alert that was already triggered
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 2 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 1 to 2, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert was already triggered at the beginning of this test")
}
verify(t, service, 2, 0, true, "The alert was already triggered at the beginning of this test")
}
func TestHandleAlertingWhenTriggeredAlertIsResolvedButSendOnResolvedIsFalse(t *testing.T) {
@@ -332,15 +163,7 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedButSendOnResolvedIsFalse(t *t
}
HandleAlerting(service, &core.Result{Success: true})
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've decreased from 1 to 0, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 1 {
t.Fatal("service.NumberOfSuccessesInARow should've increased from 0 to 1, got", service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't be triggered anymore")
}
verify(t, service, 0, 1, false, "The alert should've been resolved")
}
func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
@@ -372,24 +195,138 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
}
HandleAlerting(service, &core.Result{Success: false})
if service.NumberOfFailuresInARow != 1 {
t.Fatal("service.NumberOfFailuresInARow should've increased from 0 to 1, got", service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != 0 {
t.Fatal("service.NumberOfSuccessesInARow should've stayed at 0, got", service.NumberOfSuccessesInARow)
}
if !service.Alerts[0].Triggered {
t.Fatal("The alert should've been triggered")
}
verify(t, service, 1, 0, true, "")
HandleAlerting(service, &core.Result{Success: true})
if service.NumberOfFailuresInARow != 0 {
t.Fatal("service.NumberOfFailuresInARow should've decreased from 1 to 0, got", service.NumberOfFailuresInARow)
verify(t, service, 0, 1, false, "The alert should've been resolved")
}
func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
defer os.Clearenv()
cfg := &config.Config{
Debug: true,
Alerting: &alerting.Config{
Custom: &custom.AlertProvider{
URL: "https://twinnation.org/health",
Method: "GET",
},
},
}
if service.NumberOfSuccessesInARow != 1 {
t.Fatal("service.NumberOfSuccessesInARow should've increased from 0 to 1, got", service.NumberOfSuccessesInARow)
config.Set(cfg)
service := &core.Service{
URL: "http://example.com",
Alerts: []*core.Alert{
{
Type: core.CustomAlert,
Enabled: true,
FailureThreshold: 2,
SuccessThreshold: 2,
SendOnResolved: true,
Triggered: false,
},
},
}
if service.Alerts[0].Triggered {
t.Fatal("The alert shouldn't be triggered anymore")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 1, 0, false, "")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 2, 0, false, "The alert should have failed to trigger, because the alert provider is returning an error")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 3, 0, false, "The alert should still not be triggered, because the alert provider is still returning an error")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 4, 0, false, "The alert should still not be triggered, because the alert provider is still returning an error")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 5, 0, true, "The alert should've been triggered because the alert provider is no longer returning an error")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 1, true, "The alert should've still been triggered")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 2, false, "The alert should've been resolved DESPITE THE ALERT PROVIDER RETURNING AN ERROR. See Alert.Triggered for further explanation.")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
// Make sure that everything's working as expected after a rough patch
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 1, 0, false, "")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 2, 0, true, "The alert should have triggered")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 1, true, "The alert should still be triggered")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 2, false, "The alert should have been resolved")
}
func TestHandleAlertingWithProviderThatOnlyReturnsErrorOnResolve(t *testing.T) {
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
defer os.Clearenv()
cfg := &config.Config{
Debug: true,
Alerting: &alerting.Config{
Custom: &custom.AlertProvider{
URL: "https://twinnation.org/health",
Method: "GET",
},
},
}
config.Set(cfg)
service := &core.Service{
URL: "http://example.com",
Alerts: []*core.Alert{
{
Type: core.CustomAlert,
Enabled: true,
FailureThreshold: 1,
SuccessThreshold: 1,
SendOnResolved: true,
Triggered: false,
},
},
}
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 1, 0, true, "")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 1, false, "")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 1, 0, true, "")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 1, false, "")
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
// Make sure that everything's working as expected after a rough patch
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 1, 0, true, "")
HandleAlerting(service, &core.Result{Success: false})
verify(t, service, 2, 0, true, "")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 1, false, "")
HandleAlerting(service, &core.Result{Success: true})
verify(t, service, 0, 2, false, "")
}
func verify(t *testing.T, service *core.Service, expectedNumberOfFailuresInARow, expectedNumberOfSuccessInARow int, expectedTriggered bool, expectedTriggeredReason string) {
if service.NumberOfFailuresInARow != expectedNumberOfFailuresInARow {
t.Fatalf("service.NumberOfFailuresInARow should've been %d, got %d", expectedNumberOfFailuresInARow, service.NumberOfFailuresInARow)
}
if service.NumberOfSuccessesInARow != expectedNumberOfSuccessInARow {
t.Fatalf("service.NumberOfSuccessesInARow should've been %d, got %d", expectedNumberOfSuccessInARow, service.NumberOfSuccessesInARow)
}
if service.Alerts[0].Triggered != expectedTriggered {
if len(expectedTriggeredReason) != 0 {
t.Fatal(expectedTriggeredReason)
} else {
if expectedTriggered {
t.Fatal("The alert should've been triggered")
} else {
t.Fatal("The alert shouldn't have been triggered")
}
}
}
}