mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-07 08:34:15 +01:00
9d151fcdb4
* refactor: Partially break core package into dns, result and ssh packages * refactor: Move core package to config/endpoint * refactor: Fix warning about overlapping imported package name with endpoint variable * refactor: Rename EndpointStatus to Status * refactor: Merge result pkg back into endpoint pkg, because it makes more sense * refactor: Rename parameter r to result in Condition.evaluate * refactor: Rename parameter r to result * refactor: Revert accidental change to endpoint.TypeDNS * refactor: Rename parameter r to result * refactor: Merge util package into endpoint package * refactor: Rename parameter r to result
433 lines
13 KiB
Go
433 lines
13 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
|
"github.com/TwiN/gatus/v5/pattern"
|
|
"github.com/TwiN/gatus/v5/test"
|
|
)
|
|
|
|
func TestGetHTTPClient(t *testing.T) {
|
|
cfg := &Config{
|
|
Insecure: false,
|
|
IgnoreRedirect: false,
|
|
Timeout: 0,
|
|
DNSResolver: "tcp://1.1.1.1:53",
|
|
OAuth2Config: &OAuth2Config{
|
|
ClientID: "00000000-0000-0000-0000-000000000000",
|
|
ClientSecret: "secretsauce",
|
|
TokenURL: "https://token-server.local/token",
|
|
Scopes: []string{"https://application.local/.default"},
|
|
},
|
|
}
|
|
err := cfg.ValidateAndSetDefaults()
|
|
if err != nil {
|
|
t.Errorf("expected error to be nil, but got: `%s`", err)
|
|
}
|
|
if GetHTTPClient(cfg) == nil {
|
|
t.Error("expected client to not be nil")
|
|
}
|
|
if GetHTTPClient(nil) == nil {
|
|
t.Error("expected client to not be nil")
|
|
}
|
|
}
|
|
|
|
func TestGetDomainExpiration(t *testing.T) {
|
|
t.Parallel()
|
|
if domainExpiration, err := GetDomainExpiration("example.com"); err != nil {
|
|
t.Fatalf("expected error to be nil, but got: `%s`", err)
|
|
} else if domainExpiration <= 0 {
|
|
t.Error("expected domain expiration to be higher than 0")
|
|
}
|
|
if domainExpiration, err := GetDomainExpiration("example.com"); err != nil {
|
|
t.Errorf("expected error to be nil, but got: `%s`", err)
|
|
} else if domainExpiration <= 0 {
|
|
t.Error("expected domain expiration to be higher than 0")
|
|
}
|
|
// Hack to pretend like the domain is expiring in 1 hour, which should trigger a refresh
|
|
whoisExpirationDateCache.SetWithTTL("example.com", time.Now().Add(time.Hour), 25*time.Hour)
|
|
if domainExpiration, err := GetDomainExpiration("example.com"); err != nil {
|
|
t.Errorf("expected error to be nil, but got: `%s`", err)
|
|
} else if domainExpiration <= 0 {
|
|
t.Error("expected domain expiration to be higher than 0")
|
|
}
|
|
// Make sure the refresh works when the ttl is <24 hours
|
|
whoisExpirationDateCache.SetWithTTL("example.com", time.Now().Add(35*time.Hour), 23*time.Hour)
|
|
if domainExpiration, err := GetDomainExpiration("example.com"); err != nil {
|
|
t.Errorf("expected error to be nil, but got: `%s`", err)
|
|
} else if domainExpiration <= 0 {
|
|
t.Error("expected domain expiration to be higher than 0")
|
|
}
|
|
}
|
|
|
|
func TestPing(t *testing.T) {
|
|
t.Parallel()
|
|
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond}); !success {
|
|
t.Error("expected true")
|
|
if rtt == 0 {
|
|
t.Error("Round-trip time returned on success should've higher than 0")
|
|
}
|
|
}
|
|
if success, rtt := Ping("256.256.256.256", &Config{Timeout: 500 * time.Millisecond}); success {
|
|
t.Error("expected false, because the IP is invalid")
|
|
if rtt != 0 {
|
|
t.Error("Round-trip time returned on failure should've been 0")
|
|
}
|
|
}
|
|
if success, rtt := Ping("192.168.152.153", &Config{Timeout: 500 * time.Millisecond}); success {
|
|
t.Error("expected false, because the IP is valid but the host should be unreachable")
|
|
if rtt != 0 {
|
|
t.Error("Round-trip time returned on failure should've been 0")
|
|
}
|
|
}
|
|
// Can't perform integration tests (e.g. pinging public targets by single-stacked hostname) here,
|
|
// because ICMP is blocked in the network of GitHub-hosted runners.
|
|
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
|
|
t.Error("expected true")
|
|
if rtt == 0 {
|
|
t.Error("Round-trip time returned on failure should've been 0")
|
|
}
|
|
}
|
|
if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
|
|
t.Error("expected true")
|
|
if rtt == 0 {
|
|
t.Error("Round-trip time returned on failure should've been 0")
|
|
}
|
|
}
|
|
if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip4"}); success {
|
|
t.Error("expected false, because the IP isn't an IPv4 address")
|
|
if rtt != 0 {
|
|
t.Error("Round-trip time returned on failure should've been 0")
|
|
}
|
|
}
|
|
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip6"}); success {
|
|
t.Error("expected false, because the IP isn't an IPv6 address")
|
|
if rtt != 0 {
|
|
t.Error("Round-trip time returned on failure should've been 0")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCanPerformStartTLS(t *testing.T) {
|
|
type args struct {
|
|
address string
|
|
insecure bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantConnected bool
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "invalid address",
|
|
args: args{
|
|
address: "test",
|
|
},
|
|
wantConnected: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "error dial",
|
|
args: args{
|
|
address: "test:1234",
|
|
},
|
|
wantConnected: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid starttls",
|
|
args: args{
|
|
address: "smtp.gmail.com:587",
|
|
},
|
|
wantConnected: true,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if connected != tt.wantConnected {
|
|
t.Errorf("CanPerformStartTLS() connected=%v, wantConnected=%v", connected, tt.wantConnected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCanPerformTLS(t *testing.T) {
|
|
type args struct {
|
|
address string
|
|
insecure bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantConnected bool
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "invalid address",
|
|
args: args{
|
|
address: "test",
|
|
},
|
|
wantConnected: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "error dial",
|
|
args: args{
|
|
address: "test:1234",
|
|
},
|
|
wantConnected: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid tls",
|
|
args: args{
|
|
address: "smtp.gmail.com:465",
|
|
},
|
|
wantConnected: true,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
connected, _, err := CanPerformTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if connected != tt.wantConnected {
|
|
t.Errorf("CanPerformTLS() connected=%v, wantConnected=%v", connected, tt.wantConnected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCanCreateTCPConnection(t *testing.T) {
|
|
if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
|
|
t.Error("should've failed, because there's no port in the address")
|
|
}
|
|
if !CanCreateTCPConnection("1.1.1.1:53", &Config{Timeout: 5 * time.Second}) {
|
|
t.Error("should've succeeded, because that IP should always™ be up")
|
|
}
|
|
}
|
|
|
|
// This test checks if a HTTP client configured with `configureOAuth2()` automatically
|
|
// performs a Client Credentials OAuth2 flow and adds the obtained token as a `Authorization`
|
|
// header to all outgoing HTTP calls.
|
|
func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
|
|
defer InjectHTTPClient(nil)
|
|
oAuth2Config := &OAuth2Config{
|
|
ClientID: "00000000-0000-0000-0000-000000000000",
|
|
ClientSecret: "secretsauce",
|
|
TokenURL: "https://token-server.local/token",
|
|
Scopes: []string{"https://application.local/.default"},
|
|
}
|
|
mockHttpClient := &http.Client{
|
|
Transport: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
|
// if the mock HTTP client tries to get a token from the `token-server`
|
|
// we provide the expected token response
|
|
if r.Host == "token-server.local" {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(
|
|
[]byte(
|
|
`{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"secret-token"}`,
|
|
),
|
|
)),
|
|
}
|
|
}
|
|
// to verify the headers were sent as expected, we echo them back in the
|
|
// `X-Org-Authorization` header and check if the token value matches our
|
|
// mocked `token-server` response
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: map[string][]string{
|
|
"X-Org-Authorization": {r.Header.Get("Authorization")},
|
|
},
|
|
Body: http.NoBody,
|
|
}
|
|
}),
|
|
}
|
|
mockHttpClientWithOAuth := configureOAuth2(mockHttpClient, *oAuth2Config)
|
|
InjectHTTPClient(mockHttpClientWithOAuth)
|
|
request, err := http.NewRequest(http.MethodPost, "http://127.0.0.1:8282", http.NoBody)
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
response, err := mockHttpClientWithOAuth.Do(request)
|
|
if err != nil {
|
|
t.Error("expected no error, got", err.Error())
|
|
}
|
|
if response.Header == nil {
|
|
t.Error("expected response headers, but got nil")
|
|
}
|
|
// the mock response echos the Authorization header used in the request back
|
|
// to us as `X-Org-Authorization` header, we check here if the value matches
|
|
// our expected token `secret-token`
|
|
if response.Header.Get("X-Org-Authorization") != "Bearer secret-token" {
|
|
t.Error("expected `secret-token` as Bearer token in the mocked response header `X-Org-Authorization`, but got", response.Header.Get("X-Org-Authorization"))
|
|
}
|
|
}
|
|
|
|
func TestQueryWebSocket(t *testing.T) {
|
|
_, _, err := QueryWebSocket("", "body", &Config{Timeout: 2 * time.Second})
|
|
if err == nil {
|
|
t.Error("expected an error due to the address being invalid")
|
|
}
|
|
_, _, err = QueryWebSocket("ws://example.org", "body", &Config{Timeout: 2 * time.Second})
|
|
if err == nil {
|
|
t.Error("expected an error due to the target not being websocket-friendly")
|
|
}
|
|
}
|
|
|
|
func TestTlsRenegotiation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg TLSConfig
|
|
expectedConfig tls.RenegotiationSupport
|
|
}{
|
|
{
|
|
name: "default",
|
|
cfg: TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
|
expectedConfig: tls.RenegotiateNever,
|
|
},
|
|
{
|
|
name: "never",
|
|
cfg: TLSConfig{RenegotiationSupport: "never", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
|
expectedConfig: tls.RenegotiateNever,
|
|
},
|
|
{
|
|
name: "once",
|
|
cfg: TLSConfig{RenegotiationSupport: "once", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
|
expectedConfig: tls.RenegotiateOnceAsClient,
|
|
},
|
|
{
|
|
name: "freely",
|
|
cfg: TLSConfig{RenegotiationSupport: "freely", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
|
expectedConfig: tls.RenegotiateFreelyAsClient,
|
|
},
|
|
{
|
|
name: "not-valid-and-broken",
|
|
cfg: TLSConfig{RenegotiationSupport: "invalid", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
|
expectedConfig: tls.RenegotiateNever,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tls := &tls.Config{}
|
|
tlsConfig := configureTLS(tls, test.cfg)
|
|
if tlsConfig.Renegotiation != test.expectedConfig {
|
|
t.Errorf("expected tls renegotiation to be %v, but got %v", test.expectedConfig, tls.Renegotiation)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryDNS(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inputDNS dns.Config
|
|
inputURL string
|
|
expectedDNSCode string
|
|
expectedBody string
|
|
isErrExpected bool
|
|
}{
|
|
{
|
|
name: "test Config with type A",
|
|
inputDNS: dns.Config{
|
|
QueryType: "A",
|
|
QueryName: "example.com.",
|
|
},
|
|
inputURL: "8.8.8.8",
|
|
expectedDNSCode: "NOERROR",
|
|
expectedBody: "93.184.215.14",
|
|
},
|
|
{
|
|
name: "test Config with type AAAA",
|
|
inputDNS: dns.Config{
|
|
QueryType: "AAAA",
|
|
QueryName: "example.com.",
|
|
},
|
|
inputURL: "8.8.8.8",
|
|
expectedDNSCode: "NOERROR",
|
|
expectedBody: "2606:2800:21f:cb07:6820:80da:af6b:8b2c",
|
|
},
|
|
{
|
|
name: "test Config with type CNAME",
|
|
inputDNS: dns.Config{
|
|
QueryType: "CNAME",
|
|
QueryName: "en.wikipedia.org.",
|
|
},
|
|
inputURL: "8.8.8.8",
|
|
expectedDNSCode: "NOERROR",
|
|
expectedBody: "dyna.wikimedia.org.",
|
|
},
|
|
{
|
|
name: "test Config with type MX",
|
|
inputDNS: dns.Config{
|
|
QueryType: "MX",
|
|
QueryName: "example.com.",
|
|
},
|
|
inputURL: "8.8.8.8",
|
|
expectedDNSCode: "NOERROR",
|
|
expectedBody: ".",
|
|
},
|
|
{
|
|
name: "test Config with type NS",
|
|
inputDNS: dns.Config{
|
|
QueryType: "NS",
|
|
QueryName: "example.com.",
|
|
},
|
|
inputURL: "8.8.8.8",
|
|
expectedDNSCode: "NOERROR",
|
|
expectedBody: "*.iana-servers.net.",
|
|
},
|
|
{
|
|
name: "test Config with fake type and retrieve error",
|
|
inputDNS: dns.Config{
|
|
QueryType: "B",
|
|
QueryName: "example",
|
|
},
|
|
inputURL: "8.8.8.8",
|
|
isErrExpected: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, dnsRCode, body, err := QueryDNS(test.inputDNS.QueryType, test.inputDNS.QueryName, test.inputURL)
|
|
if test.isErrExpected && err == nil {
|
|
t.Errorf("there should be an error")
|
|
}
|
|
if dnsRCode != test.expectedDNSCode {
|
|
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, dnsRCode)
|
|
}
|
|
if test.inputDNS.QueryType == "NS" {
|
|
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
|
if !pattern.Match(test.expectedBody, string(body)) {
|
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
|
}
|
|
} else {
|
|
if string(body) != test.expectedBody {
|
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
|
}
|
|
}
|
|
})
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|