* test(config): make HasLoadedConfigurationBeenModified deterministic The two subtests each slept a full second so a rewritten file's mod time (second precision) would advance past the load time. Advance the mod time explicitly with os.Chtimes instead, and give the directory subtest its own temp dir so the file subtest's future-dated config.yaml no longer leaks into it. Removes ~2s of wall-clock sleep and the coarse-clock dependence; the test is now deterministic. * test(config): trim comments to the forward-facing rationale --------- Co-authored-by: TwiN <twin@linux.com>
2647 lines
87 KiB
Go
2647 lines
87 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/TwiN/gatus/v5/alerting"
|
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
|
"github.com/TwiN/gatus/v5/alerting/provider"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/clickup"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/datadog"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/gitea"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/homeassistant"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/ifttt"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/ilert"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/incidentio"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/line"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/newrelic"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/plivo"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/rocketchat"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/sendgrid"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/signal"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/signl4"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/splunk"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/squadcast"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/vonage"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/webex"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/zapier"
|
|
"github.com/TwiN/gatus/v5/alerting/provider/zulip"
|
|
"github.com/TwiN/gatus/v5/client"
|
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
|
"github.com/TwiN/gatus/v5/config/suite"
|
|
"github.com/TwiN/gatus/v5/config/tunneling"
|
|
"github.com/TwiN/gatus/v5/config/tunneling/sshtunnel"
|
|
"github.com/TwiN/gatus/v5/config/web"
|
|
"github.com/TwiN/gatus/v5/storage"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestLoadConfiguration(t *testing.T) {
|
|
yes := true
|
|
dir := t.TempDir()
|
|
scenarios := []struct {
|
|
name string
|
|
configPath string // value to pass as the configPath parameter in LoadConfiguration
|
|
pathAndFiles map[string]string // files to create in dir
|
|
expectedConfig *Config
|
|
expectedError error
|
|
}{
|
|
{
|
|
name: "empty-config-file",
|
|
configPath: filepath.Join(dir, "config.yaml"),
|
|
pathAndFiles: map[string]string{
|
|
"config.yaml": "",
|
|
},
|
|
expectedError: ErrConfigFileNotFound,
|
|
},
|
|
{
|
|
name: "config-file-that-does-not-exist",
|
|
configPath: filepath.Join(dir, "config.yaml"),
|
|
expectedError: ErrConfigFileNotFound,
|
|
},
|
|
{
|
|
name: "config-file-with-endpoint-that-has-no-url",
|
|
configPath: filepath.Join(dir, "config.yaml"),
|
|
pathAndFiles: map[string]string{
|
|
"config.yaml": `
|
|
endpoints:
|
|
- name: website`,
|
|
},
|
|
expectedError: endpoint.ErrEndpointWithNoURL,
|
|
},
|
|
{
|
|
name: "config-file-with-endpoint-that-has-no-conditions",
|
|
configPath: filepath.Join(dir, "config.yaml"),
|
|
pathAndFiles: map[string]string{
|
|
"config.yaml": `
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health`,
|
|
},
|
|
expectedError: endpoint.ErrEndpointWithNoCondition,
|
|
},
|
|
{
|
|
name: "config-file",
|
|
configPath: filepath.Join(dir, "config.yaml"),
|
|
pathAndFiles: map[string]string{
|
|
"config.yaml": `
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
expectedConfig: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "website",
|
|
URL: "https://twin.sh/health",
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty-dir",
|
|
configPath: dir,
|
|
pathAndFiles: map[string]string{},
|
|
expectedError: ErrConfigFileNotFound,
|
|
},
|
|
{
|
|
name: "dir-with-empty-config-file",
|
|
configPath: dir,
|
|
pathAndFiles: map[string]string{
|
|
"config.yaml": "",
|
|
},
|
|
expectedError: ErrNoEndpointOrSuiteInConfig,
|
|
},
|
|
{
|
|
name: "dir-with-two-config-files",
|
|
configPath: dir,
|
|
pathAndFiles: map[string]string{
|
|
"config.yaml": `endpoints:
|
|
- name: one
|
|
url: https://example.com
|
|
conditions:
|
|
- "[CONNECTED] == true"
|
|
- "[STATUS] == 200"
|
|
|
|
- name: two
|
|
url: https://example.org
|
|
conditions:
|
|
- "len([BODY]) > 0"`,
|
|
"config.yml": `endpoints:
|
|
- name: three
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- "[BODY].status == UP"`,
|
|
},
|
|
expectedConfig: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "one",
|
|
URL: "https://example.com",
|
|
Conditions: []endpoint.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
|
},
|
|
{
|
|
Name: "two",
|
|
URL: "https://example.org",
|
|
Conditions: []endpoint.Condition{"len([BODY]) > 0"},
|
|
},
|
|
{
|
|
Name: "three",
|
|
URL: "https://twin.sh/health",
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "dir-with-2-config-files-deep-merge-with-map-slice-and-primitive",
|
|
configPath: dir,
|
|
pathAndFiles: map[string]string{
|
|
"a.yaml": `
|
|
metrics: true
|
|
|
|
alerting:
|
|
slack:
|
|
webhook-url: https://hooks.slack.com/services/xxx/yyy/zzz
|
|
default-alert:
|
|
enabled: true
|
|
|
|
endpoints:
|
|
- name: example
|
|
url: https://example.org
|
|
interval: 5s
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
"b.yaml": `
|
|
|
|
alerting:
|
|
discord:
|
|
webhook-url: https://discord.com/api/webhooks/xxx/yyy
|
|
|
|
external-endpoints:
|
|
- name: ext-ep-test
|
|
token: "potato"
|
|
alerts:
|
|
- type: slack
|
|
|
|
endpoints:
|
|
- name: frontend
|
|
url: https://example.com
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
expectedConfig: &Config{
|
|
Metrics: true,
|
|
Alerting: &alerting.Config{
|
|
Discord: &discord.AlertProvider{DefaultConfig: discord.Config{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"}},
|
|
Slack: &slack.AlertProvider{DefaultConfig: slack.Config{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"}, DefaultAlert: &alert.Alert{Enabled: &yes}},
|
|
},
|
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
|
{
|
|
Name: "ext-ep-test",
|
|
Token: "potato",
|
|
Alerts: []*alert.Alert{
|
|
{
|
|
Type: alert.TypeSlack,
|
|
FailureThreshold: 3,
|
|
SuccessThreshold: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "example",
|
|
URL: "https://example.org",
|
|
Interval: 5 * time.Second,
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
{
|
|
Name: "frontend",
|
|
URL: "https://example.com",
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
for path, content := range scenario.pathAndFiles {
|
|
if err := os.WriteFile(filepath.Join(dir, path), []byte(content), 0o644); err != nil {
|
|
t.Fatalf("[%s] failed to write file: %v", scenario.name, err)
|
|
}
|
|
}
|
|
defer func(pathAndFiles map[string]string) {
|
|
for path := range pathAndFiles {
|
|
_ = os.Remove(filepath.Join(dir, path))
|
|
}
|
|
}(scenario.pathAndFiles)
|
|
config, err := LoadConfiguration(scenario.configPath)
|
|
if !errors.Is(err, scenario.expectedError) {
|
|
t.Errorf("[%s] expected error %v, got %v", scenario.name, scenario.expectedError, err)
|
|
return
|
|
} else if err != nil && errors.Is(err, scenario.expectedError) {
|
|
return
|
|
}
|
|
// parse the expected output so that expectations are closer to reality (under the right circumstances, even I can be poetic)
|
|
expectedConfigAsYAML, _ := yaml.Marshal(scenario.expectedConfig)
|
|
expectedConfigAfterBeingParsedAndValidated, err := parseAndValidateConfigBytes(expectedConfigAsYAML)
|
|
if err != nil {
|
|
t.Fatalf("[%s] failed to parse expected config: %v", scenario.name, err)
|
|
}
|
|
// Marshal em' before comparing em' so that we don't have to deal with formatting and ordering
|
|
actualConfigAsYAML, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
t.Fatalf("[%s] failed to marshal actual config: %v", scenario.name, err)
|
|
}
|
|
expectedConfigAfterBeingParsedAndValidatedAsYAML, _ := yaml.Marshal(expectedConfigAfterBeingParsedAndValidated)
|
|
if string(actualConfigAsYAML) != string(expectedConfigAfterBeingParsedAndValidatedAsYAML) {
|
|
t.Errorf("[%s] expected config %s, got %s", scenario.name, string(expectedConfigAfterBeingParsedAndValidatedAsYAML), string(actualConfigAsYAML))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfig_HasLoadedConfigurationBeenModified(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
configFilePath := filepath.Join(dir, "config.yaml")
|
|
_ = os.WriteFile(configFilePath, []byte(`endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`), 0o644)
|
|
|
|
t.Run("config-file-as-config-path", func(t *testing.T) {
|
|
config, err := LoadConfiguration(configFilePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to load configuration: %v", err)
|
|
}
|
|
if config.HasLoadedConfigurationBeenModified() {
|
|
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return false because nothing has happened since it was created")
|
|
}
|
|
if err = os.WriteFile(configFilePath, []byte(`endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`), 0o644); err != nil {
|
|
t.Fatalf("failed to overwrite config file: %v", err)
|
|
}
|
|
// File mod times have second precision, so advance it explicitly.
|
|
future := time.Now().Add(2 * time.Second)
|
|
if err = os.Chtimes(configFilePath, future, future); err != nil {
|
|
t.Fatalf("failed to advance config file mod time: %v", err)
|
|
}
|
|
if !config.HasLoadedConfigurationBeenModified() {
|
|
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return true because the config file has been modified")
|
|
}
|
|
})
|
|
t.Run("config-directory-as-config-path", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(`endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
config, err := LoadConfiguration(dir)
|
|
if err != nil {
|
|
t.Fatalf("failed to load configuration: %v", err)
|
|
}
|
|
if config.HasLoadedConfigurationBeenModified() {
|
|
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return false because nothing has happened since it was created")
|
|
}
|
|
if err = os.WriteFile(filepath.Join(dir, "metrics.yaml"), []byte(`metrics: true`), 0o644); err != nil {
|
|
t.Fatalf("failed to add config file: %v", err)
|
|
}
|
|
// File mod times have second precision, so advance it explicitly.
|
|
future := time.Now().Add(2 * time.Second)
|
|
if err = os.Chtimes(filepath.Join(dir, "metrics.yaml"), future, future); err != nil {
|
|
t.Fatalf("failed to advance config file mod time: %v", err)
|
|
}
|
|
if !config.HasLoadedConfigurationBeenModified() {
|
|
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return true because a new file has been added in the directory")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytes(t *testing.T) {
|
|
file := t.TempDir() + "/test.db"
|
|
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
|
storage:
|
|
type: sqlite
|
|
path: %s
|
|
maximum-number-of-results: 10
|
|
maximum-number-of-events: 5
|
|
|
|
maintenance:
|
|
enabled: true
|
|
start: 00:00
|
|
duration: 4h
|
|
every: [Monday, Thursday]
|
|
|
|
ui:
|
|
title: T
|
|
header: H
|
|
link: https://example.org
|
|
buttons:
|
|
- name: "Home"
|
|
link: "https://example.org"
|
|
- name: "Status page"
|
|
link: "https://status.example.org"
|
|
|
|
external-endpoints:
|
|
- name: ext-ep-test
|
|
group: core
|
|
token: "potato"
|
|
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
interval: 15s
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
- name: github
|
|
url: https://api.github.com/healthz
|
|
client:
|
|
insecure: true
|
|
ignore-redirect: true
|
|
timeout: 5s
|
|
conditions:
|
|
- "[STATUS] != 400"
|
|
- "[STATUS] != 500"
|
|
|
|
- name: example
|
|
url: https://example.com/
|
|
interval: 30m
|
|
client:
|
|
insecure: true
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`, file)))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite {
|
|
t.Error("expected storage to be set to sqlite, got", config.Storage)
|
|
}
|
|
if config.Storage == nil || config.Storage.MaximumNumberOfResults != 10 || config.Storage.MaximumNumberOfEvents != 5 {
|
|
t.Error("expected MaximumNumberOfResults and MaximumNumberOfEvents to be set to 10 and 5, got", config.Storage.MaximumNumberOfResults, config.Storage.MaximumNumberOfEvents)
|
|
}
|
|
if config.UI == nil || config.UI.Title != "T" || config.UI.Header != "H" || config.UI.Link != "https://example.org" || len(config.UI.Buttons) != 2 || config.UI.Buttons[0].Name != "Home" || config.UI.Buttons[0].Link != "https://example.org" || config.UI.Buttons[1].Name != "Status page" || config.UI.Buttons[1].Link != "https://status.example.org" {
|
|
t.Error("expected ui to be set to T, H, https://example.org, 2 buttons, Home and Status page, got", config.UI)
|
|
}
|
|
if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 {
|
|
t.Error("Expected Config.Maintenance to be configured properly")
|
|
}
|
|
if len(config.ExternalEndpoints) != 1 {
|
|
t.Error("Should have returned one external endpoint")
|
|
}
|
|
if config.ExternalEndpoints[0].Name != "ext-ep-test" {
|
|
t.Errorf("Name should have been %s", "ext-ep-test")
|
|
}
|
|
if config.ExternalEndpoints[0].Group != "core" {
|
|
t.Errorf("Group should have been %s", "core")
|
|
}
|
|
if config.ExternalEndpoints[0].Token != "potato" {
|
|
t.Errorf("Token should have been %s", "potato")
|
|
}
|
|
|
|
if len(config.Endpoints) != 3 {
|
|
t.Error("Should have returned two endpoints")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[0].Method != "GET" {
|
|
t.Errorf("Method should have been %s (default)", "GET")
|
|
}
|
|
if config.Endpoints[0].Interval != 15*time.Second {
|
|
t.Errorf("Interval should have been %s", 15*time.Second)
|
|
}
|
|
if config.Endpoints[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
|
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Endpoints[0].ClientConfig.Insecure)
|
|
}
|
|
if config.Endpoints[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
|
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v, got %v", true, config.Endpoints[0].ClientConfig.IgnoreRedirect)
|
|
}
|
|
if config.Endpoints[0].ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
|
|
t.Errorf("ClientConfig.Timeout should have been %v, got %v", client.GetDefaultConfig().Timeout, config.Endpoints[0].ClientConfig.Timeout)
|
|
}
|
|
if len(config.Endpoints[0].Conditions) != 1 {
|
|
t.Errorf("There should have been %d conditions", 1)
|
|
}
|
|
if config.Endpoints[1].URL != "https://api.github.com/healthz" {
|
|
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
|
|
}
|
|
if config.Endpoints[1].Method != "GET" {
|
|
t.Errorf("Method should have been %s (default)", "GET")
|
|
}
|
|
if config.Endpoints[1].Interval != 60*time.Second {
|
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
|
}
|
|
if !config.Endpoints[1].ClientConfig.Insecure {
|
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Endpoints[1].ClientConfig.Insecure)
|
|
}
|
|
if !config.Endpoints[1].ClientConfig.IgnoreRedirect {
|
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v, got %v", true, config.Endpoints[1].ClientConfig.IgnoreRedirect)
|
|
}
|
|
if config.Endpoints[1].ClientConfig.Timeout != 5*time.Second {
|
|
t.Errorf("ClientConfig.Timeout should have been %v, got %v", 5*time.Second, config.Endpoints[1].ClientConfig.Timeout)
|
|
}
|
|
if len(config.Endpoints[1].Conditions) != 2 {
|
|
t.Errorf("There should have been %d conditions", 2)
|
|
}
|
|
if config.Endpoints[2].URL != "https://example.com/" {
|
|
t.Errorf("URL should have been %s", "https://example.com/")
|
|
}
|
|
if config.Endpoints[2].Method != "GET" {
|
|
t.Errorf("Method should have been %s (default)", "GET")
|
|
}
|
|
if config.Endpoints[2].Interval != 30*time.Minute {
|
|
t.Errorf("Interval should have been %s, because it is the default value", 30*time.Minute)
|
|
}
|
|
if !config.Endpoints[2].ClientConfig.Insecure {
|
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Endpoints[2].ClientConfig.Insecure)
|
|
}
|
|
if config.Endpoints[2].ClientConfig.IgnoreRedirect {
|
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v by default, got %v", false, config.Endpoints[2].ClientConfig.IgnoreRedirect)
|
|
}
|
|
if config.Endpoints[2].ClientConfig.Timeout != 10*time.Second {
|
|
t.Errorf("ClientConfig.Timeout should have been %v by default, got %v", 10*time.Second, config.Endpoints[2].ClientConfig.Timeout)
|
|
}
|
|
if len(config.Endpoints[2].Conditions) != 1 {
|
|
t.Errorf("There should have been %d conditions", 1)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesDefault(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("DefaultConfig shouldn't have been nil")
|
|
}
|
|
if config.Metrics {
|
|
t.Error("Metrics should've been false by default")
|
|
}
|
|
if config.Web.Address != web.DefaultAddress {
|
|
t.Errorf("Bind address should have been %s, because it is the default value", web.DefaultAddress)
|
|
}
|
|
if config.Web.Port != web.DefaultPort {
|
|
t.Errorf("Port should have been %d, because it is the default value", web.DefaultPort)
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[0].Interval != 60*time.Second {
|
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
|
}
|
|
if config.Endpoints[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
|
|
t.Errorf("ClientConfig.Insecure should have been %v by default, got %v", true, config.Endpoints[0].ClientConfig.Insecure)
|
|
}
|
|
if config.Endpoints[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
|
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v by default, got %v", true, config.Endpoints[0].ClientConfig.IgnoreRedirect)
|
|
}
|
|
if config.Endpoints[0].ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
|
|
t.Errorf("ClientConfig.Timeout should have been %v by default, got %v", client.GetDefaultConfig().Timeout, config.Endpoints[0].ClientConfig.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithAddress(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
web:
|
|
address: 127.0.0.1
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/actuator/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Metrics {
|
|
t.Error("Metrics should've been false by default")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/actuator/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/actuator/health")
|
|
}
|
|
if config.Endpoints[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 != "127.0.0.1" {
|
|
t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1")
|
|
}
|
|
if config.Web.Port != web.DefaultPort {
|
|
t.Errorf("Port should have been %d, because it is the default value", web.DefaultPort)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithPort(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
web:
|
|
port: 12345
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Metrics {
|
|
t.Error("Metrics should've been false by default")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[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 != web.DefaultAddress {
|
|
t.Errorf("Bind address should have been %s, because it is the default value", web.DefaultAddress)
|
|
}
|
|
if config.Web.Port != 12345 {
|
|
t.Errorf("Port should have been %d, because it is specified in config", 12345)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithPortAndHost(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
web:
|
|
port: 12345
|
|
address: 127.0.0.1
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Metrics {
|
|
t.Error("Metrics should've been false by default")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[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 != "127.0.0.1" {
|
|
t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1")
|
|
}
|
|
if config.Web.Port != 12345 {
|
|
t.Errorf("Port should have been %d, because it is specified in config", 12345)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidPort(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(`
|
|
web:
|
|
port: 65536
|
|
address: 127.0.0.1
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("Should've returned an error because the configuration specifies an invalid port value")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithMetricsAndCustomUserAgentHeader(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
metrics: true
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
headers:
|
|
User-Agent: Test/2.0
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if !config.Metrics {
|
|
t.Error("Metrics should have been true")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[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 != web.DefaultAddress {
|
|
t.Errorf("Bind address should have been %s, because it is the default value", web.DefaultAddress)
|
|
}
|
|
if config.Web.Port != web.DefaultPort {
|
|
t.Errorf("Port should have been %d, because it is the default value", web.DefaultPort)
|
|
}
|
|
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != "Test/2.0" {
|
|
t.Errorf("User-Agent should've been %s, got %s", "Test/2.0", userAgent)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithMetricsAndHostAndPort(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
metrics: true
|
|
web:
|
|
address: 192.168.0.1
|
|
port: 9090
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if !config.Metrics {
|
|
t.Error("Metrics should have been true")
|
|
}
|
|
if config.Web.Address != "192.168.0.1" {
|
|
t.Errorf("Bind address should have been %s, because it is the default value", "192.168.0.1")
|
|
}
|
|
if config.Web.Port != 9090 {
|
|
t.Errorf("Port should have been %d, because it is specified in config", 9090)
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[0].Interval != 60*time.Second {
|
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
|
}
|
|
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != endpoint.GatusUserAgent {
|
|
t.Errorf("User-Agent should've been %s because it's the default value, got %s", endpoint.GatusUserAgent, userAgent)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateBadConfigBytes(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(`
|
|
badconfig:
|
|
- asdsa: w0w
|
|
usadasdrl: asdxzczxc
|
|
asdas:
|
|
- soup
|
|
`))
|
|
if err == nil {
|
|
t.Error("An error should've been returned")
|
|
}
|
|
if err != ErrNoEndpointOrSuiteInConfig {
|
|
t.Error("The error returned should have been of type ErrNoEndpointOrSuiteInConfig")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithAlerting(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
slack:
|
|
webhook-url: "http://example.com"
|
|
discord:
|
|
webhook-url: "http://example.org"
|
|
pagerduty:
|
|
integration-key: "00000000000000000000000000000000"
|
|
pushover:
|
|
application-token: "000000000000000000000000000000"
|
|
user-key: "000000000000000000000000000000"
|
|
mattermost:
|
|
webhook-url: "http://example.com"
|
|
client:
|
|
insecure: true
|
|
messagebird:
|
|
access-key: "1"
|
|
originator: "31619191918"
|
|
recipients: "31619191919"
|
|
telegram:
|
|
token: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
|
id: 0123456789
|
|
twilio:
|
|
sid: "1234"
|
|
token: "5678"
|
|
from: "+1-234-567-8901"
|
|
to: "+1-234-567-8901"
|
|
teams:
|
|
webhook-url: "http://example.com"
|
|
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: slack
|
|
- type: pagerduty
|
|
failure-threshold: 7
|
|
success-threshold: 5
|
|
description: "Healthcheck failed 7 times in a row"
|
|
- type: mattermost
|
|
- type: messagebird
|
|
enabled: false
|
|
- type: discord
|
|
failure-threshold: 10
|
|
- type: telegram
|
|
enabled: true
|
|
- type: twilio
|
|
failure-threshold: 12
|
|
success-threshold: 15
|
|
- type: teams
|
|
- type: pushover
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
// Alerting providers
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.Slack == nil || config.Alerting.Slack.Validate() != nil {
|
|
t.Fatal("Slack alerting config should've been valid")
|
|
}
|
|
// Endpoints
|
|
if len(config.Endpoints) != 1 {
|
|
t.Error("There should've been 1 endpoint")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[0].Interval != 60*time.Second {
|
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
|
}
|
|
if len(config.Endpoints[0].Alerts) != 9 {
|
|
t.Fatal("There should've been 9 alerts configured")
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Endpoints[0].Alerts[0].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[0].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[0].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[0].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[0].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[0].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[1].Type != alert.TypePagerDuty {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePagerDuty, config.Endpoints[0].Alerts[1].Type)
|
|
}
|
|
if config.Endpoints[0].Alerts[1].GetDescription() != "Healthcheck failed 7 times in a row" {
|
|
t.Errorf("The description of the alert should've been %s, but it was %s", "Healthcheck failed 7 times in a row", config.Endpoints[0].Alerts[1].GetDescription())
|
|
}
|
|
if config.Endpoints[0].Alerts[1].FailureThreshold != 7 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 7, config.Endpoints[0].Alerts[1].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[1].SuccessThreshold != 5 {
|
|
t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Endpoints[0].Alerts[1].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[2].Type != alert.TypeMattermost {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMattermost, config.Endpoints[0].Alerts[2].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[2].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[2].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[2].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[2].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[2].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[3].Type != alert.TypeMessagebird {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMessagebird, config.Endpoints[0].Alerts[3].Type)
|
|
}
|
|
if config.Endpoints[0].Alerts[3].IsEnabled() {
|
|
t.Error("The alert should've been disabled")
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[4].Type != alert.TypeDiscord {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.Endpoints[0].Alerts[4].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[4].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[4].FailureThreshold != 10 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[4].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[4].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[4].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[5].Type != alert.TypeTelegram {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTelegram, config.Endpoints[0].Alerts[5].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[5].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[5].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[5].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[5].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[5].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[6].Type != alert.TypeTwilio {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTwilio, config.Endpoints[0].Alerts[6].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[6].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[6].FailureThreshold != 12 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 12, config.Endpoints[0].Alerts[6].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[6].SuccessThreshold != 15 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Endpoints[0].Alerts[6].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[7].Type != alert.TypeTeams {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTeams, config.Endpoints[0].Alerts[7].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[7].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[7].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[7].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[7].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[8].Type != alert.TypePushover {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[8].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
slack:
|
|
webhook-url: "http://example.com"
|
|
default-alert:
|
|
enabled: true
|
|
discord:
|
|
webhook-url: "http://example.org"
|
|
default-alert:
|
|
enabled: true
|
|
failure-threshold: 10
|
|
success-threshold: 15
|
|
pagerduty:
|
|
integration-key: "00000000000000000000000000000000"
|
|
default-alert:
|
|
enabled: true
|
|
description: default description
|
|
failure-threshold: 7
|
|
success-threshold: 5
|
|
pushover:
|
|
application-token: "000000000000000000000000000000"
|
|
user-key: "000000000000000000000000000000"
|
|
default-alert:
|
|
enabled: true
|
|
description: default description
|
|
failure-threshold: 5
|
|
success-threshold: 3
|
|
mattermost:
|
|
webhook-url: "http://example.com"
|
|
default-alert:
|
|
enabled: true
|
|
messagebird:
|
|
access-key: "1"
|
|
originator: "31619191918"
|
|
recipients: "31619191919"
|
|
default-alert:
|
|
enabled: false
|
|
send-on-resolved: true
|
|
telegram:
|
|
token: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
|
id: 0123456789
|
|
default-alert:
|
|
enabled: true
|
|
twilio:
|
|
sid: "1234"
|
|
token: "5678"
|
|
from: "+1-234-567-8901"
|
|
to: "+1-234-567-8901"
|
|
default-alert:
|
|
enabled: true
|
|
failure-threshold: 12
|
|
success-threshold: 15
|
|
teams:
|
|
webhook-url: "http://example.com"
|
|
default-alert:
|
|
enabled: true
|
|
email:
|
|
from: "from@example.com"
|
|
username: "from@example.com"
|
|
password: "hunter2"
|
|
host: "mail.example.com"
|
|
port: 587
|
|
to: "recipient1@example.com,recipient2@example.com"
|
|
client:
|
|
insecure: false
|
|
default-alert:
|
|
enabled: true
|
|
gotify:
|
|
server-url: "https://gotify.example"
|
|
token: "**************"
|
|
default-alert:
|
|
enabled: true
|
|
|
|
external-endpoints:
|
|
- name: ext-ep-test
|
|
group: core
|
|
token: potato
|
|
alerts:
|
|
- type: discord
|
|
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: slack
|
|
- type: pagerduty
|
|
- type: mattermost
|
|
- type: messagebird
|
|
- type: discord
|
|
success-threshold: 8 # test endpoint alert override
|
|
- type: telegram
|
|
- type: twilio
|
|
- type: teams
|
|
- type: pushover
|
|
- type: email
|
|
- type: gotify
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Metrics {
|
|
t.Error("Metrics should've been false by default")
|
|
}
|
|
// Alerting providers
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
|
|
if config.Alerting.Slack == nil || config.Alerting.Slack.Validate() != nil {
|
|
t.Fatal("Slack alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Slack.GetDefaultAlert() == nil {
|
|
t.Fatal("Slack.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Slack.DefaultConfig.WebhookURL != "http://example.com" {
|
|
t.Errorf("Slack webhook should've been %s, but was %s", "http://example.com", config.Alerting.Slack.DefaultConfig.WebhookURL)
|
|
}
|
|
|
|
if config.Alerting.PagerDuty == nil || config.Alerting.PagerDuty.Validate() != nil {
|
|
t.Fatal("PagerDuty alerting config should've been valid")
|
|
}
|
|
if config.Alerting.PagerDuty.GetDefaultAlert() == nil {
|
|
t.Fatal("PagerDuty.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.PagerDuty.DefaultConfig.IntegrationKey != "00000000000000000000000000000000" {
|
|
t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.DefaultConfig.IntegrationKey)
|
|
}
|
|
|
|
if config.Alerting.Pushover == nil || config.Alerting.Pushover.Validate() != nil {
|
|
t.Fatal("Pushover alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Pushover.GetDefaultAlert() == nil {
|
|
t.Fatal("Pushover.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Pushover.DefaultConfig.ApplicationToken != "000000000000000000000000000000" {
|
|
t.Errorf("Pushover application token should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.DefaultConfig.ApplicationToken)
|
|
}
|
|
if config.Alerting.Pushover.DefaultConfig.UserKey != "000000000000000000000000000000" {
|
|
t.Errorf("Pushover user key should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.DefaultConfig.UserKey)
|
|
}
|
|
|
|
if config.Alerting.Mattermost == nil || config.Alerting.Mattermost.Validate() != nil {
|
|
t.Fatal("Mattermost alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Mattermost.GetDefaultAlert() == nil {
|
|
t.Fatal("Mattermost.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
|
|
if config.Alerting.Messagebird == nil || config.Alerting.Messagebird.Validate() != nil {
|
|
t.Fatal("Messagebird alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Messagebird.GetDefaultAlert() == nil {
|
|
t.Fatal("Messagebird.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Messagebird.DefaultConfig.AccessKey != "1" {
|
|
t.Errorf("Messagebird access key should've been %s, but was %s", "1", config.Alerting.Messagebird.DefaultConfig.AccessKey)
|
|
}
|
|
if config.Alerting.Messagebird.DefaultConfig.Originator != "31619191918" {
|
|
t.Errorf("Messagebird originator field should've been %s, but was %s", "31619191918", config.Alerting.Messagebird.DefaultConfig.Originator)
|
|
}
|
|
if config.Alerting.Messagebird.DefaultConfig.Recipients != "31619191919" {
|
|
t.Errorf("Messagebird to recipients should've been %s, but was %s", "31619191919", config.Alerting.Messagebird.DefaultConfig.Recipients)
|
|
}
|
|
|
|
if config.Alerting.Discord == nil || config.Alerting.Discord.Validate() != nil {
|
|
t.Fatal("Discord alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Discord.GetDefaultAlert() == nil {
|
|
t.Fatal("Discord.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Discord.GetDefaultAlert().FailureThreshold != 10 {
|
|
t.Errorf("Discord default alert failure threshold should've been %d, but was %d", 10, config.Alerting.Discord.GetDefaultAlert().FailureThreshold)
|
|
}
|
|
if config.Alerting.Discord.GetDefaultAlert().SuccessThreshold != 15 {
|
|
t.Errorf("Discord default alert success threshold should've been %d, but was %d", 15, config.Alerting.Discord.GetDefaultAlert().SuccessThreshold)
|
|
}
|
|
if config.Alerting.Discord.DefaultConfig.WebhookURL != "http://example.org" {
|
|
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.DefaultConfig.WebhookURL)
|
|
}
|
|
if config.Alerting.GetAlertingProviderByAlertType(alert.TypeDiscord) != config.Alerting.Discord {
|
|
t.Error("expected discord configuration")
|
|
}
|
|
|
|
if config.Alerting.Telegram == nil || config.Alerting.Telegram.Validate() != nil {
|
|
t.Fatal("Telegram alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Telegram.GetDefaultAlert() == nil {
|
|
t.Fatal("Telegram.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Telegram.DefaultConfig.Token != "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" {
|
|
t.Errorf("Telegram token should've been %s, but was %s", "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", config.Alerting.Telegram.DefaultConfig.Token)
|
|
}
|
|
if config.Alerting.Telegram.DefaultConfig.ID != "0123456789" {
|
|
t.Errorf("Telegram ID should've been %s, but was %s", "012345689", config.Alerting.Telegram.DefaultConfig.ID)
|
|
}
|
|
|
|
if config.Alerting.Twilio == nil || config.Alerting.Twilio.Validate() != nil {
|
|
t.Fatal("Twilio alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Twilio.GetDefaultAlert() == nil {
|
|
t.Fatal("Twilio.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
|
|
if config.Alerting.Teams == nil || config.Alerting.Teams.Validate() != nil {
|
|
t.Fatal("Teams alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Teams.GetDefaultAlert() == nil {
|
|
t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
|
|
if config.Alerting.Email == nil || config.Alerting.Email.Validate() != nil {
|
|
t.Fatal("Email alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Email.GetDefaultAlert() == nil {
|
|
t.Fatal("Email.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.From != "from@example.com" {
|
|
t.Errorf("Email from should've been %s, but was %s", "from@example.com", config.Alerting.Email.DefaultConfig.From)
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.Username != "from@example.com" {
|
|
t.Errorf("Email username should've been %s, but was %s", "from@example.com", config.Alerting.Email.DefaultConfig.Username)
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.Password != "hunter2" {
|
|
t.Errorf("Email password should've been %s, but was %s", "hunter2", config.Alerting.Email.DefaultConfig.Password)
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.Host != "mail.example.com" {
|
|
t.Errorf("Email host should've been %s, but was %s", "mail.example.com", config.Alerting.Email.DefaultConfig.Host)
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.Port != 587 {
|
|
t.Errorf("Email port should've been %d, but was %d", 587, config.Alerting.Email.DefaultConfig.Port)
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.To != "recipient1@example.com,recipient2@example.com" {
|
|
t.Errorf("Email to should've been %s, but was %s", "recipient1@example.com,recipient2@example.com", config.Alerting.Email.DefaultConfig.To)
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.ClientConfig == nil {
|
|
t.Fatal("Email client config should've been set")
|
|
}
|
|
if config.Alerting.Email.DefaultConfig.ClientConfig.Insecure {
|
|
t.Error("Email client config should've been secure")
|
|
}
|
|
|
|
if config.Alerting.Gotify == nil || config.Alerting.Gotify.Validate() != nil {
|
|
t.Fatal("Gotify alerting config should've been valid")
|
|
}
|
|
if config.Alerting.Gotify.GetDefaultAlert() == nil {
|
|
t.Fatal("Gotify.GetDefaultAlert() shouldn't have returned nil")
|
|
}
|
|
if config.Alerting.Gotify.DefaultConfig.ServerURL != "https://gotify.example" {
|
|
t.Errorf("Gotify server URL should've been %s, but was %s", "https://gotify.example", config.Alerting.Gotify.DefaultConfig.ServerURL)
|
|
}
|
|
if config.Alerting.Gotify.DefaultConfig.Token != "**************" {
|
|
t.Errorf("Gotify token should've been %s, but was %s", "**************", config.Alerting.Gotify.DefaultConfig.Token)
|
|
}
|
|
|
|
// External endpoints
|
|
if len(config.ExternalEndpoints) != 1 {
|
|
t.Error("There should've been 1 external endpoint")
|
|
}
|
|
if config.ExternalEndpoints[0].Alerts[0].Type != alert.TypeDiscord {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.ExternalEndpoints[0].Alerts[0].Type)
|
|
}
|
|
if !config.ExternalEndpoints[0].Alerts[0].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.ExternalEndpoints[0].Alerts[0].FailureThreshold != 10 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.ExternalEndpoints[0].Alerts[0].FailureThreshold)
|
|
}
|
|
if config.ExternalEndpoints[0].Alerts[0].SuccessThreshold != 15 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.ExternalEndpoints[0].Alerts[0].SuccessThreshold)
|
|
}
|
|
|
|
// Endpoints
|
|
if len(config.Endpoints) != 1 {
|
|
t.Error("There should've been 1 endpoint")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[0].Interval != 60*time.Second {
|
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
|
}
|
|
if len(config.Endpoints[0].Alerts) != 11 {
|
|
t.Fatalf("There should've been 11 alerts configured, got %d", len(config.Endpoints[0].Alerts))
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Endpoints[0].Alerts[0].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[0].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[0].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[0].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[0].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[0].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[1].Type != alert.TypePagerDuty {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePagerDuty, config.Endpoints[0].Alerts[1].Type)
|
|
}
|
|
if config.Endpoints[0].Alerts[1].GetDescription() != "default description" {
|
|
t.Errorf("The description of the alert should've been %s, but it was %s", "default description", config.Endpoints[0].Alerts[1].GetDescription())
|
|
}
|
|
if config.Endpoints[0].Alerts[1].FailureThreshold != 7 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 7, config.Endpoints[0].Alerts[1].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[1].SuccessThreshold != 5 {
|
|
t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Endpoints[0].Alerts[1].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[2].Type != alert.TypeMattermost {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMattermost, config.Endpoints[0].Alerts[2].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[2].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[2].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[2].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[2].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[2].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[3].Type != alert.TypeMessagebird {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMessagebird, config.Endpoints[0].Alerts[3].Type)
|
|
}
|
|
if config.Endpoints[0].Alerts[3].IsEnabled() {
|
|
t.Error("The alert should've been disabled")
|
|
}
|
|
if !config.Endpoints[0].Alerts[3].IsSendingOnResolved() {
|
|
t.Error("The alert should be sending on resolve")
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[4].Type != alert.TypeDiscord {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.Endpoints[0].Alerts[4].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[4].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[4].FailureThreshold != 10 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[4].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[4].SuccessThreshold != 8 {
|
|
t.Errorf("The default success threshold of the alert should've been %d because it was explicitly overriden, but it was %d", 8, config.Endpoints[0].Alerts[4].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[5].Type != alert.TypeTelegram {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTelegram, config.Endpoints[0].Alerts[5].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[5].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[5].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[5].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[5].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[5].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[6].Type != alert.TypeTwilio {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTwilio, config.Endpoints[0].Alerts[6].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[6].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[6].FailureThreshold != 12 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 12, config.Endpoints[0].Alerts[6].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[6].SuccessThreshold != 15 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Endpoints[0].Alerts[6].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[7].Type != alert.TypeTeams {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTeams, config.Endpoints[0].Alerts[7].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[7].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[7].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[7].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[7].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[8].Type != alert.TypePushover {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[8].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[8].FailureThreshold != 5 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[8].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[8].SuccessThreshold != 3 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[9].Type != alert.TypeEmail {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeEmail, config.Endpoints[0].Alerts[9].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[9].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[9].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[9].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[9].SuccessThreshold)
|
|
}
|
|
|
|
if config.Endpoints[0].Alerts[10].Type != alert.TypeGotify {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeGotify, config.Endpoints[0].Alerts[10].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[10].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[10].FailureThreshold != 3 {
|
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[10].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[10].SuccessThreshold != 2 {
|
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[10].SuccessThreshold)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
slack:
|
|
webhook-url: "https://example.com"
|
|
default-alert:
|
|
enabled: true
|
|
description: "description"
|
|
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: slack
|
|
failure-threshold: 10
|
|
- type: slack
|
|
failure-threshold: 20
|
|
description: "wow"
|
|
- type: slack
|
|
enabled: false
|
|
failure-threshold: 30
|
|
provider-override:
|
|
webhook-url: https://example.com
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
// Alerting providers
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.Slack == nil || config.Alerting.Slack.Validate() != nil {
|
|
t.Fatal("Slack alerting config should've been valid")
|
|
}
|
|
// Endpoints
|
|
if len(config.Endpoints) != 1 {
|
|
t.Error("There should've been 2 endpoints")
|
|
}
|
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Endpoints[0].Alerts[0].Type)
|
|
}
|
|
if config.Endpoints[0].Alerts[1].Type != alert.TypeSlack {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Endpoints[0].Alerts[1].Type)
|
|
}
|
|
if config.Endpoints[0].Alerts[2].Type != alert.TypeSlack {
|
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Endpoints[0].Alerts[2].Type)
|
|
}
|
|
if !config.Endpoints[0].Alerts[0].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if !config.Endpoints[0].Alerts[1].IsEnabled() {
|
|
t.Error("The alert should've been enabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[2].IsEnabled() {
|
|
t.Error("The alert should've been disabled")
|
|
}
|
|
if config.Endpoints[0].Alerts[0].GetDescription() != "description" {
|
|
t.Errorf("The description of the alert should've been %s, but it was %s", "description", config.Endpoints[0].Alerts[0].GetDescription())
|
|
}
|
|
if config.Endpoints[0].Alerts[1].GetDescription() != "wow" {
|
|
t.Errorf("The description of the alert should've been %s, but it was %s", "description", config.Endpoints[0].Alerts[1].GetDescription())
|
|
}
|
|
if config.Endpoints[0].Alerts[2].GetDescription() != "description" {
|
|
t.Errorf("The description of the alert should've been %s, but it was %s", "description", config.Endpoints[0].Alerts[2].GetDescription())
|
|
}
|
|
if config.Endpoints[0].Alerts[0].FailureThreshold != 10 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[0].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[1].FailureThreshold != 20 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 20, config.Endpoints[0].Alerts[1].FailureThreshold)
|
|
}
|
|
if config.Endpoints[0].Alerts[2].FailureThreshold != 30 {
|
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 30, config.Endpoints[0].Alerts[2].FailureThreshold)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidPagerDutyAlertingConfig(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
pagerduty:
|
|
integration-key: "INVALID_KEY"
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: pagerduty
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.PagerDuty != nil {
|
|
t.Fatal("PagerDuty alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidPushoverAlertingConfig(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
pushover:
|
|
application-token: "INVALID_TOKEN"
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: pushover
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.Pushover != nil {
|
|
t.Fatal("Pushover alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithCustomAlertingConfig(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
custom:
|
|
url: "https://example.com"
|
|
body: |
|
|
{
|
|
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
|
}
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: custom
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("Config shouldn't have been nil")
|
|
}
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.Custom == nil {
|
|
t.Fatal("Custom alerting config shouldn't have been nil")
|
|
}
|
|
if err = config.Alerting.Custom.Validate(); err != nil {
|
|
t.Fatal("Custom alerting config should've been valid")
|
|
}
|
|
cfg, _ := config.Alerting.Custom.GetConfig("", &alert.Alert{ProviderOverride: map[string]any{"client": map[string]any{"insecure": true}}})
|
|
if config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, true) != "RESOLVED" {
|
|
t.Fatal("ALERT_TRIGGERED_OR_RESOLVED placeholder value for RESOLVED should've been 'RESOLVED', got", config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, true))
|
|
}
|
|
if config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, false) != "TRIGGERED" {
|
|
t.Fatal("ALERT_TRIGGERED_OR_RESOLVED placeholder value for TRIGGERED should've been 'TRIGGERED', got", config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, false))
|
|
}
|
|
if !cfg.ClientConfig.Insecure {
|
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, cfg.ClientConfig.Insecure)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithCustomAlertingConfigAndCustomPlaceholderValues(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
custom:
|
|
placeholders:
|
|
ALERT_TRIGGERED_OR_RESOLVED:
|
|
TRIGGERED: "partial_outage"
|
|
RESOLVED: "operational"
|
|
url: "https://example.com"
|
|
insecure: true
|
|
body: "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: custom
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("DefaultConfig shouldn't have been nil")
|
|
}
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.Custom == nil {
|
|
t.Fatal("Custom alerting config shouldn't have been nil")
|
|
}
|
|
if err = config.Alerting.Custom.Validate(); err != nil {
|
|
t.Fatal("Custom alerting config should've been valid")
|
|
}
|
|
cfg, _ := config.Alerting.Custom.GetConfig("", &alert.Alert{})
|
|
if config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, true) != "operational" {
|
|
t.Fatal("ALERT_TRIGGERED_OR_RESOLVED placeholder value for RESOLVED should've been 'operational'")
|
|
}
|
|
if config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, false) != "partial_outage" {
|
|
t.Fatal("ALERT_TRIGGERED_OR_RESOLVED placeholder value for TRIGGERED should've been 'partial_outage'")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithCustomAlertingConfigAndOneCustomPlaceholderValue(t *testing.T) {
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
alerting:
|
|
custom:
|
|
placeholders:
|
|
ALERT_TRIGGERED_OR_RESOLVED:
|
|
TRIGGERED: "partial_outage"
|
|
url: "https://example.com"
|
|
body: "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
alerts:
|
|
- type: custom
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("DefaultConfig shouldn't have been nil")
|
|
}
|
|
if config.Alerting == nil {
|
|
t.Fatal("config.Alerting shouldn't have been nil")
|
|
}
|
|
if config.Alerting.Custom == nil {
|
|
t.Fatal("Custom alerting config shouldn't have been nil")
|
|
}
|
|
if err := config.Alerting.Custom.Validate(); err != nil {
|
|
t.Fatal("Custom alerting config should've been valid")
|
|
}
|
|
cfg, _ := config.Alerting.Custom.GetConfig("", &alert.Alert{})
|
|
if config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, true) != "RESOLVED" {
|
|
t.Fatal("ALERT_TRIGGERED_OR_RESOLVED placeholder value for RESOLVED should've been 'RESOLVED'")
|
|
}
|
|
if config.Alerting.Custom.GetAlertStatePlaceholderValue(cfg, false) != "partial_outage" {
|
|
t.Fatal("ALERT_TRIGGERED_OR_RESOLVED placeholder value for TRIGGERED should've been 'partial_outage'")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidEndpointName(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(`
|
|
endpoints:
|
|
- name: ""
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err == nil {
|
|
t.Error("should've returned an error")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithDuplicateEndpointName(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
shouldError bool
|
|
config string
|
|
}{
|
|
{
|
|
name: "same-name-no-group",
|
|
shouldError: true,
|
|
config: `
|
|
endpoints:
|
|
- name: ep1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- name: ep1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "same-name-different-group",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: ep1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- name: ep1
|
|
group: g1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "same-name-same-group",
|
|
shouldError: true,
|
|
config: `
|
|
endpoints:
|
|
- name: ep1
|
|
group: g1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- name: ep1
|
|
group: g1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "same-name-different-endpoint-type",
|
|
shouldError: true,
|
|
config: `
|
|
external-endpoints:
|
|
- name: ep1
|
|
token: "12345678"
|
|
|
|
endpoints:
|
|
- name: ep1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "same-name-different-group-different-endpoint-type",
|
|
shouldError: false,
|
|
config: `
|
|
external-endpoints:
|
|
- name: ep1
|
|
group: gr1
|
|
token: "12345678"
|
|
|
|
endpoints:
|
|
- name: ep1
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
}
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
|
if scenario.shouldError && err == nil {
|
|
t.Error("should've returned an error")
|
|
} else if !scenario.shouldError && err != nil {
|
|
t.Error("shouldn't have returned an error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidStorageConfig(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(`
|
|
storage:
|
|
type: sqlite
|
|
endpoints:
|
|
- name: example
|
|
url: https://example.org
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err == nil {
|
|
t.Error("should've returned an error, because a file must be specified for a storage of type sqlite")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidYAML(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(`
|
|
storage:
|
|
invalid yaml
|
|
endpoints:
|
|
- name: example
|
|
url: https://example.org
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err == nil {
|
|
t.Error("should've returned an error")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithInvalidSecurityConfig(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(`
|
|
security:
|
|
basic:
|
|
username: "admin"
|
|
password-sha512: "invalid-sha512-hash"
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`))
|
|
if err == nil {
|
|
t.Error("should've returned an error")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithValidSecurityConfig(t *testing.T) {
|
|
const expectedUsername = "admin"
|
|
const expectedPasswordHash = "JDJhJDEwJHRiMnRFakxWazZLdXBzRERQazB1TE8vckRLY05Yb1hSdnoxWU0yQ1FaYXZRSW1McmladDYu"
|
|
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
|
security:
|
|
basic:
|
|
username: "%s"
|
|
password-bcrypt-base64: "%s"
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
`, expectedUsername, expectedPasswordHash)))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("DefaultConfig shouldn't have been nil")
|
|
}
|
|
if config.Security == nil {
|
|
t.Fatal("config.Security shouldn't have been nil")
|
|
}
|
|
if !config.Security.ValidateAndSetDefaults() {
|
|
t.Error("Security config should've been valid")
|
|
}
|
|
if config.Security.Basic == nil {
|
|
t.Fatal("config.Security.Basic shouldn't have been nil")
|
|
}
|
|
if config.Security.Basic.Username != expectedUsername {
|
|
t.Errorf("config.Security.Basic.Username should've been %s, but was %s", expectedUsername, config.Security.Basic.Username)
|
|
}
|
|
if config.Security.Basic.PasswordBcryptHashBase64Encoded != expectedPasswordHash {
|
|
t.Errorf("config.Security.Basic.PasswordBcryptHashBase64Encoded should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordBcryptHashBase64Encoded)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithLiteralDollarSign(t *testing.T) {
|
|
os.Setenv("GATUS_TestParseAndValidateConfigBytesWithLiteralDollarSign", "whatever")
|
|
config, err := parseAndValidateConfigBytes([]byte(`
|
|
endpoints:
|
|
- name: website
|
|
url: https://twin.sh/health
|
|
conditions:
|
|
- "[BODY] == $$GATUS_TestParseAndValidateConfigBytesWithLiteralDollarSign"
|
|
- "[BODY] == $GATUS_TestParseAndValidateConfigBytesWithLiteralDollarSign"
|
|
`))
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if config == nil {
|
|
t.Fatal("DefaultConfig shouldn't have been nil")
|
|
}
|
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
|
}
|
|
if config.Endpoints[0].Conditions[0] != "[BODY] == $GATUS_TestParseAndValidateConfigBytesWithLiteralDollarSign" {
|
|
t.Errorf("Condition should have been %s", "[BODY] == $GATUS_TestParseAndValidateConfigBytesWithLiteralDollarSign")
|
|
}
|
|
if config.Endpoints[0].Conditions[1] != "[BODY] == whatever" {
|
|
t.Errorf("Condition should have been %s", "[BODY] == whatever")
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithNoEndpoints(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(``))
|
|
if !errors.Is(err, ErrNoEndpointOrSuiteInConfig) {
|
|
t.Error("The error returned should have been of type ErrNoEndpointOrSuiteInConfig")
|
|
}
|
|
}
|
|
|
|
func TestGetAlertingProviderByAlertType(t *testing.T) {
|
|
alertingConfig := &alerting.Config{
|
|
AWSSimpleEmailService: &awsses.AlertProvider{},
|
|
ClickUp: &clickup.AlertProvider{},
|
|
Custom: &custom.AlertProvider{},
|
|
Datadog: &datadog.AlertProvider{},
|
|
Discord: &discord.AlertProvider{},
|
|
Email: &email.AlertProvider{},
|
|
Gitea: &gitea.AlertProvider{},
|
|
GitHub: &github.AlertProvider{},
|
|
GitLab: &gitlab.AlertProvider{},
|
|
GoogleChat: &googlechat.AlertProvider{},
|
|
Gotify: &gotify.AlertProvider{},
|
|
HomeAssistant: &homeassistant.AlertProvider{},
|
|
IFTTT: &ifttt.AlertProvider{},
|
|
Ilert: &ilert.AlertProvider{},
|
|
IncidentIO: &incidentio.AlertProvider{},
|
|
Line: &line.AlertProvider{},
|
|
Matrix: &matrix.AlertProvider{},
|
|
Mattermost: &mattermost.AlertProvider{},
|
|
Messagebird: &messagebird.AlertProvider{},
|
|
NewRelic: &newrelic.AlertProvider{},
|
|
Ntfy: &ntfy.AlertProvider{},
|
|
Opsgenie: &opsgenie.AlertProvider{},
|
|
PagerDuty: &pagerduty.AlertProvider{},
|
|
Plivo: &plivo.AlertProvider{},
|
|
Pushover: &pushover.AlertProvider{},
|
|
RocketChat: &rocketchat.AlertProvider{},
|
|
SendGrid: &sendgrid.AlertProvider{},
|
|
Signal: &signal.AlertProvider{},
|
|
SIGNL4: &signl4.AlertProvider{},
|
|
Slack: &slack.AlertProvider{},
|
|
Splunk: &splunk.AlertProvider{},
|
|
Squadcast: &squadcast.AlertProvider{},
|
|
Telegram: &telegram.AlertProvider{},
|
|
Teams: &teams.AlertProvider{},
|
|
TeamsWorkflows: &teamsworkflows.AlertProvider{},
|
|
Twilio: &twilio.AlertProvider{},
|
|
Vonage: &vonage.AlertProvider{},
|
|
Webex: &webex.AlertProvider{},
|
|
Zapier: &zapier.AlertProvider{},
|
|
Zulip: &zulip.AlertProvider{},
|
|
}
|
|
scenarios := []struct {
|
|
alertType alert.Type
|
|
expected provider.AlertProvider
|
|
}{
|
|
{alertType: alert.TypeAWSSES, expected: alertingConfig.AWSSimpleEmailService},
|
|
{alertType: alert.TypeClickUp, expected: alertingConfig.ClickUp},
|
|
{alertType: alert.TypeCustom, expected: alertingConfig.Custom},
|
|
{alertType: alert.TypeDatadog, expected: alertingConfig.Datadog},
|
|
{alertType: alert.TypeDiscord, expected: alertingConfig.Discord},
|
|
{alertType: alert.TypeEmail, expected: alertingConfig.Email},
|
|
{alertType: alert.TypeGitea, expected: alertingConfig.Gitea},
|
|
{alertType: alert.TypeGitHub, expected: alertingConfig.GitHub},
|
|
{alertType: alert.TypeGitLab, expected: alertingConfig.GitLab},
|
|
{alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat},
|
|
{alertType: alert.TypeGotify, expected: alertingConfig.Gotify},
|
|
{alertType: alert.TypeHomeAssistant, expected: alertingConfig.HomeAssistant},
|
|
{alertType: alert.TypeIFTTT, expected: alertingConfig.IFTTT},
|
|
{alertType: alert.TypeIlert, expected: alertingConfig.Ilert},
|
|
{alertType: alert.TypeIncidentIO, expected: alertingConfig.IncidentIO},
|
|
{alertType: alert.TypeLine, expected: alertingConfig.Line},
|
|
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
|
|
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
|
|
{alertType: alert.TypeMessagebird, expected: alertingConfig.Messagebird},
|
|
{alertType: alert.TypeNewRelic, expected: alertingConfig.NewRelic},
|
|
{alertType: alert.TypeNtfy, expected: alertingConfig.Ntfy},
|
|
{alertType: alert.TypeOpsgenie, expected: alertingConfig.Opsgenie},
|
|
{alertType: alert.TypePagerDuty, expected: alertingConfig.PagerDuty},
|
|
{alertType: alert.TypePlivo, expected: alertingConfig.Plivo},
|
|
{alertType: alert.TypePushover, expected: alertingConfig.Pushover},
|
|
{alertType: alert.TypeRocketChat, expected: alertingConfig.RocketChat},
|
|
{alertType: alert.TypeSendGrid, expected: alertingConfig.SendGrid},
|
|
{alertType: alert.TypeSignal, expected: alertingConfig.Signal},
|
|
{alertType: alert.TypeSIGNL4, expected: alertingConfig.SIGNL4},
|
|
{alertType: alert.TypeSlack, expected: alertingConfig.Slack},
|
|
{alertType: alert.TypeSplunk, expected: alertingConfig.Splunk},
|
|
{alertType: alert.TypeSquadcast, expected: alertingConfig.Squadcast},
|
|
{alertType: alert.TypeTelegram, expected: alertingConfig.Telegram},
|
|
{alertType: alert.TypeTeams, expected: alertingConfig.Teams},
|
|
{alertType: alert.TypeTeamsWorkflows, expected: alertingConfig.TeamsWorkflows},
|
|
{alertType: alert.TypeTwilio, expected: alertingConfig.Twilio},
|
|
{alertType: alert.TypeVonage, expected: alertingConfig.Vonage},
|
|
{alertType: alert.TypeWebex, expected: alertingConfig.Webex},
|
|
{alertType: alert.TypeZapier, expected: alertingConfig.Zapier},
|
|
{alertType: alert.TypeZulip, expected: alertingConfig.Zulip},
|
|
}
|
|
for _, scenario := range scenarios {
|
|
t.Run(string(scenario.alertType), func(t *testing.T) {
|
|
if alertingConfig.GetAlertingProviderByAlertType(scenario.alertType) != scenario.expected {
|
|
t.Errorf("expected %s configuration", scenario.alertType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfig_GetUniqueExtraMetricLabels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "no-endpoints",
|
|
config: &Config{
|
|
Endpoints: []*endpoint.Endpoint{},
|
|
},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "single-endpoint-no-labels",
|
|
config: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "endpoint1",
|
|
URL: "https://example.com",
|
|
},
|
|
},
|
|
},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "single-endpoint-with-labels",
|
|
config: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "endpoint1",
|
|
URL: "https://example.com",
|
|
Enabled: toPtr(true),
|
|
ExtraLabels: map[string]string{
|
|
"env": "production",
|
|
"team": "backend",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: []string{"env", "team"},
|
|
},
|
|
{
|
|
name: "multiple-endpoints-with-labels",
|
|
config: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "endpoint1",
|
|
URL: "https://example.com",
|
|
Enabled: toPtr(true),
|
|
ExtraLabels: map[string]string{
|
|
"env": "production",
|
|
"team": "backend",
|
|
"module": "auth",
|
|
},
|
|
},
|
|
{
|
|
Name: "endpoint2",
|
|
URL: "https://example.org",
|
|
Enabled: toPtr(true),
|
|
ExtraLabels: map[string]string{
|
|
"env": "staging",
|
|
"team": "frontend",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: []string{"env", "team", "module"},
|
|
},
|
|
{
|
|
name: "multiple-endpoints-with-some-disabled",
|
|
config: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "endpoint1",
|
|
URL: "https://example.com",
|
|
Enabled: toPtr(true),
|
|
ExtraLabels: map[string]string{
|
|
"env": "production",
|
|
"team": "backend",
|
|
},
|
|
},
|
|
{
|
|
Name: "endpoint2",
|
|
URL: "https://example.org",
|
|
Enabled: toPtr(false),
|
|
ExtraLabels: map[string]string{
|
|
"module": "auth",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: []string{"env", "team"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
labels := tt.config.GetUniqueExtraMetricLabels()
|
|
if len(labels) != len(tt.expected) {
|
|
t.Errorf("expected %d labels, got %d", len(tt.expected), len(labels))
|
|
}
|
|
for _, label := range tt.expected {
|
|
if !slices.Contains(labels, label) {
|
|
t.Errorf("expected label %s to be present", label)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithDuplicateKeysAcrossEntityTypes(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
shouldError bool
|
|
expectedErr string
|
|
config string
|
|
}{
|
|
{
|
|
name: "endpoint-suite-same-key",
|
|
shouldError: true,
|
|
expectedErr: "duplicate key 'backend_test-api': suite 'backend_test-api' conflicts with endpoint 'backend_test-api'",
|
|
config: `
|
|
endpoints:
|
|
- name: test-api
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: test-api
|
|
group: backend
|
|
interval: 30s
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "endpoint-suite-different-keys",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-service
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: integration-tests
|
|
group: testing
|
|
interval: 30s
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "endpoint-external-endpoint-suite-unique-keys",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-service
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
external-endpoints:
|
|
- name: monitoring-agent
|
|
group: infrastructure
|
|
token: "secret-token"
|
|
heartbeat:
|
|
interval: 5m
|
|
|
|
suites:
|
|
- name: integration-tests
|
|
group: testing
|
|
interval: 30s
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "suite-with-same-key-as-external-endpoint",
|
|
shouldError: true,
|
|
expectedErr: "duplicate key 'monitoring_health-check': suite 'monitoring_health-check' conflicts with external endpoint 'monitoring_health-check'",
|
|
config: `
|
|
endpoints:
|
|
- name: dummy
|
|
url: https://example.com/dummy
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
external-endpoints:
|
|
- name: health-check
|
|
group: monitoring
|
|
token: "secret-token"
|
|
heartbeat:
|
|
interval: 5m
|
|
|
|
suites:
|
|
- name: health-check
|
|
group: monitoring
|
|
interval: 30s
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "endpoint-with-same-name-as-suite-endpoint-different-groups",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-health
|
|
group: backend
|
|
url: https://example.com/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: integration-suite
|
|
group: testing
|
|
interval: 30s
|
|
endpoints:
|
|
- name: api-health
|
|
url: https://example.com/api/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "endpoint-conflicting-with-suite-endpoint",
|
|
shouldError: true,
|
|
expectedErr: "duplicate key 'backend_api-health': endpoint 'backend_api-health' in suite 'backend_integration-suite' conflicts with endpoint 'backend_api-health'",
|
|
config: `
|
|
endpoints:
|
|
- name: api-health
|
|
group: backend
|
|
url: https://example.com/health
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: integration-suite
|
|
group: backend
|
|
interval: 30s
|
|
endpoints:
|
|
- name: api-health
|
|
url: https://example.com/api/health
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
|
if scenario.shouldError {
|
|
if err == nil {
|
|
t.Error("should've returned an error")
|
|
} else if scenario.expectedErr != "" && err.Error() != scenario.expectedErr {
|
|
t.Errorf("expected error message '%s', got '%s'", scenario.expectedErr, err.Error())
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("shouldn't have returned an error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateConfigBytesWithSuites(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
shouldError bool
|
|
expectedErr string
|
|
config string
|
|
}{
|
|
{
|
|
name: "suite-with-no-name",
|
|
shouldError: true,
|
|
expectedErr: "invalid suite 'testing_': suite must have a name",
|
|
config: `
|
|
endpoints:
|
|
- name: dummy
|
|
url: https://example.com/dummy
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- group: testing
|
|
interval: 30s
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "suite-with-no-endpoints",
|
|
shouldError: true,
|
|
expectedErr: "invalid suite 'testing_empty-suite': suite must have at least one endpoint",
|
|
config: `
|
|
endpoints:
|
|
- name: dummy
|
|
url: https://example.com/dummy
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: empty-suite
|
|
group: testing
|
|
interval: 30s
|
|
endpoints: []`,
|
|
},
|
|
{
|
|
name: "suite-with-duplicate-endpoint-names",
|
|
shouldError: true,
|
|
expectedErr: "invalid suite 'testing_duplicate-test': suite cannot have duplicate endpoint names: duplicate endpoint name 'step1'",
|
|
config: `
|
|
endpoints:
|
|
- name: dummy
|
|
url: https://example.com/dummy
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: duplicate-test
|
|
group: testing
|
|
interval: 30s
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test1
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- name: step1
|
|
url: https://example.com/test2
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "suite-with-invalid-negative-timeout",
|
|
shouldError: true,
|
|
expectedErr: "invalid suite 'testing_negative-timeout-suite': suite timeout must be positive",
|
|
config: `
|
|
endpoints:
|
|
- name: dummy
|
|
url: https://example.com/dummy
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: negative-timeout-suite
|
|
group: testing
|
|
interval: 30s
|
|
timeout: -5m
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "valid-suite-with-defaults",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-service
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: integration-test
|
|
group: testing
|
|
endpoints:
|
|
- name: step1
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- name: step2
|
|
url: https://example.com/validate
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "valid-suite-with-all-fields",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-service
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: full-integration-test
|
|
group: testing
|
|
enabled: true
|
|
interval: 15m
|
|
timeout: 10m
|
|
context:
|
|
base_url: "https://example.com"
|
|
user_id: 12345
|
|
endpoints:
|
|
- name: authentication
|
|
url: https://example.com/auth
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- name: user-profile
|
|
url: https://example.com/profile
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
- "[BODY].user_id == 12345"`,
|
|
},
|
|
{
|
|
name: "valid-suite-with-endpoint-inheritance",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-service
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: inheritance-test
|
|
group: parent-group
|
|
endpoints:
|
|
- name: child-endpoint
|
|
url: https://example.com/test
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
{
|
|
name: "valid-suite-with-store-functionality",
|
|
shouldError: false,
|
|
config: `
|
|
endpoints:
|
|
- name: api-service
|
|
group: backend
|
|
url: https://example.com/api
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
|
|
suites:
|
|
- name: store-test
|
|
group: testing
|
|
endpoints:
|
|
- name: get-token
|
|
url: https://example.com/auth
|
|
conditions:
|
|
- "[STATUS] == 200"
|
|
store:
|
|
auth_token: "[BODY].token"
|
|
- name: use-token
|
|
url: https://example.com/data
|
|
headers:
|
|
Authorization: "Bearer {auth_token}"
|
|
conditions:
|
|
- "[STATUS] == 200"`,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
|
if scenario.shouldError {
|
|
if err == nil {
|
|
t.Error("should've returned an error")
|
|
} else if scenario.expectedErr != "" && err.Error() != scenario.expectedErr {
|
|
t.Errorf("expected error message '%s', got '%s'", scenario.expectedErr, err.Error())
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("shouldn't have returned an error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateTunnelingConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid tunneling config",
|
|
config: &Config{
|
|
Tunneling: &tunneling.Config{
|
|
Tunnels: map[string]*sshtunnel.Config{
|
|
"test": {
|
|
Type: "SSH",
|
|
Host: "example.com",
|
|
Username: "test",
|
|
Password: "secret",
|
|
},
|
|
},
|
|
},
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "test-endpoint",
|
|
URL: "http://example.com/health",
|
|
ClientConfig: &client.Config{
|
|
Tunnel: "test",
|
|
},
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid tunnel reference in endpoint",
|
|
config: &Config{
|
|
Tunneling: &tunneling.Config{
|
|
Tunnels: map[string]*sshtunnel.Config{
|
|
"test": {
|
|
Type: "SSH",
|
|
Host: "example.com",
|
|
Username: "test",
|
|
Password: "secret",
|
|
},
|
|
},
|
|
},
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "test-endpoint",
|
|
URL: "http://example.com/health",
|
|
ClientConfig: &client.Config{
|
|
Tunnel: "nonexistent",
|
|
},
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "endpoint '_test-endpoint': tunnel 'nonexistent' not found in tunneling configuration",
|
|
},
|
|
{
|
|
name: "invalid tunnel reference in suite endpoint",
|
|
config: &Config{
|
|
Tunneling: &tunneling.Config{
|
|
Tunnels: map[string]*sshtunnel.Config{
|
|
"test": {
|
|
Type: "SSH",
|
|
Host: "example.com",
|
|
Username: "test",
|
|
Password: "secret",
|
|
},
|
|
},
|
|
},
|
|
Suites: []*suite.Suite{
|
|
{
|
|
Name: "test-suite",
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "suite-endpoint",
|
|
URL: "http://example.com/health",
|
|
ClientConfig: &client.Config{
|
|
Tunnel: "invalid",
|
|
},
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "suite '_test-suite' endpoint '_suite-endpoint': tunnel 'invalid' not found in tunneling configuration",
|
|
},
|
|
{
|
|
name: "no tunneling config",
|
|
config: &Config{
|
|
Endpoints: []*endpoint.Endpoint{
|
|
{
|
|
Name: "test-endpoint",
|
|
URL: "http://example.com/health",
|
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateTunnelingConfig(tt.config)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("ValidateTunnelingConfig() expected error but got none")
|
|
return
|
|
}
|
|
if err.Error() != tt.errMsg {
|
|
t.Errorf("ValidateTunnelingConfig() error = %v, want %v", err.Error(), tt.errMsg)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("ValidateTunnelingConfig() unexpected error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveTunnelForClientConfig(t *testing.T) {
|
|
config := &Config{
|
|
Tunneling: &tunneling.Config{
|
|
Tunnels: map[string]*sshtunnel.Config{
|
|
"test": {
|
|
Type: "SSH",
|
|
Host: "example.com",
|
|
Username: "test",
|
|
Password: "secret",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := config.Tunneling.ValidateAndSetDefaults()
|
|
if err != nil {
|
|
t.Fatalf("Failed to validate tunnel config: %v", err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
clientConfig *client.Config
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid tunnel reference",
|
|
clientConfig: &client.Config{
|
|
Tunnel: "test",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid tunnel reference",
|
|
clientConfig: &client.Config{
|
|
Tunnel: "nonexistent",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "tunnel 'nonexistent' not found in tunneling configuration",
|
|
},
|
|
{
|
|
name: "no tunnel reference",
|
|
clientConfig: &client.Config{},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := resolveTunnelForClientConfig(config, tt.clientConfig)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("resolveTunnelForClientConfig() expected error but got none")
|
|
return
|
|
}
|
|
if err.Error() != tt.errMsg {
|
|
t.Errorf("resolveTunnelForClientConfig() error = %v, want %v", err.Error(), tt.errMsg)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("resolveTunnelForClientConfig() unexpected error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|