2023-03-28 22:48:58 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
package assert
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2023-02-09 14:45:31 +00:00
|
|
|
"net/http"
|
|
|
|
"regexp"
|
2022-11-01 19:03:23 +00:00
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2023-01-20 22:02:44 +00:00
|
|
|
"github.com/hashicorp/go-cleanhttp"
|
2023-07-05 15:30:48 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2023-01-20 22:02:44 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
"github.com/hashicorp/consul/api"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
2023-05-16 19:57:24 +00:00
|
|
|
libservice "github.com/hashicorp/consul/test/integration/consul-container/libs/service"
|
2022-11-01 19:03:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-02-13 19:09:12 +00:00
|
|
|
defaultHTTPTimeout = 120 * time.Second
|
2022-11-01 19:03:23 +00:00
|
|
|
defaultHTTPWait = defaultWait
|
|
|
|
)
|
|
|
|
|
2023-01-20 22:02:44 +00:00
|
|
|
// CatalogServiceExists verifies the service name exists in the Consul catalog
|
2023-03-02 22:40:07 +00:00
|
|
|
func CatalogServiceExists(t *testing.T, c *api.Client, svc string, opts *api.QueryOptions) {
|
2023-01-20 22:02:44 +00:00
|
|
|
retry.Run(t, func(r *retry.R) {
|
2023-03-02 22:40:07 +00:00
|
|
|
services, _, err := c.Catalog().Service(svc, "", opts)
|
2023-01-20 22:02:44 +00:00
|
|
|
if err != nil {
|
2023-02-07 19:13:19 +00:00
|
|
|
r.Fatal("error reading service data")
|
2023-01-20 22:02:44 +00:00
|
|
|
}
|
|
|
|
if len(services) == 0 {
|
|
|
|
r.Fatal("did not find catalog entry for ", svc)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:39:53 +00:00
|
|
|
// CatalogServiceHasInstanceCount verifies the service name exists in the Consul catalog and has the specified
|
|
|
|
// number of instances.
|
|
|
|
func CatalogServiceHasInstanceCount(t *testing.T, c *api.Client, svc string, count int, opts *api.QueryOptions) {
|
|
|
|
retry.Run(t, func(r *retry.R) {
|
|
|
|
services, _, err := c.Catalog().Service(svc, "", opts)
|
|
|
|
if err != nil {
|
|
|
|
r.Fatal("error reading service data")
|
|
|
|
}
|
|
|
|
if len(services) != count {
|
|
|
|
r.Fatalf("did not find %d catalog entries for %s", count, svc)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-05 15:30:48 +00:00
|
|
|
// CatalogNodeExists verifies the node name exists in the Consul catalog
|
2023-02-07 19:13:19 +00:00
|
|
|
func CatalogNodeExists(t *testing.T, c *api.Client, nodeName string) {
|
|
|
|
retry.Run(t, func(r *retry.R) {
|
|
|
|
node, _, err := c.Catalog().Node(nodeName, nil)
|
|
|
|
if err != nil {
|
|
|
|
r.Fatal("error reading node data")
|
|
|
|
}
|
|
|
|
if node == nil {
|
|
|
|
r.Fatal("did not find node entry for", nodeName)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-05 15:30:48 +00:00
|
|
|
// CatalogServiceIsHealthy verifies the service name exists and all instances pass healthchecks
|
|
|
|
func CatalogServiceIsHealthy(t *testing.T, c *api.Client, svc string, opts *api.QueryOptions) {
|
|
|
|
CatalogServiceExists(t, c, svc, opts)
|
|
|
|
|
|
|
|
retry.Run(t, func(r *retry.R) {
|
|
|
|
services, _, err := c.Health().Service(svc, "", false, opts)
|
|
|
|
if err != nil {
|
|
|
|
r.Fatal("error reading service health data")
|
|
|
|
}
|
|
|
|
if len(services) == 0 {
|
|
|
|
r.Fatal("did not find catalog entry for ", svc)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, svc := range services {
|
|
|
|
for _, check := range svc.Checks {
|
|
|
|
if check.Status != api.HealthPassing {
|
|
|
|
r.Fatal("at least one check is not PASSING for service", svc.Service.Service)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-02-22 15:22:25 +00:00
|
|
|
func HTTPServiceEchoes(t *testing.T, ip string, port int, path string) {
|
2023-07-05 15:30:48 +00:00
|
|
|
doHTTPServiceEchoes(t, ip, port, path, nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func HTTPServiceEchoesWithHeaders(t *testing.T, ip string, port int, path string, headers map[string]string) {
|
|
|
|
doHTTPServiceEchoes(t, ip, port, path, headers, nil)
|
2023-02-22 15:22:25 +00:00
|
|
|
}
|
2023-07-05 15:30:48 +00:00
|
|
|
|
2023-05-16 19:57:24 +00:00
|
|
|
func HTTPServiceEchoesWithClient(t *testing.T, client *http.Client, addr string, path string) {
|
2023-07-05 15:30:48 +00:00
|
|
|
doHTTPServiceEchoesWithClient(t, client, addr, path, nil, nil)
|
2023-05-16 19:57:24 +00:00
|
|
|
}
|
2023-02-22 15:22:25 +00:00
|
|
|
|
|
|
|
func HTTPServiceEchoesResHeader(t *testing.T, ip string, port int, path string, expectedResHeader map[string]string) {
|
2023-07-05 15:30:48 +00:00
|
|
|
doHTTPServiceEchoes(t, ip, port, path, nil, expectedResHeader)
|
2023-02-22 15:22:25 +00:00
|
|
|
}
|
2023-05-16 19:57:24 +00:00
|
|
|
func HTTPServiceEchoesResHeaderWithClient(t *testing.T, client *http.Client, addr string, path string, expectedResHeader map[string]string) {
|
2023-07-05 15:30:48 +00:00
|
|
|
doHTTPServiceEchoesWithClient(t, client, addr, path, nil, expectedResHeader)
|
2023-05-16 19:57:24 +00:00
|
|
|
}
|
2023-02-22 15:22:25 +00:00
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
// HTTPServiceEchoes verifies that a post to the given ip/port combination returns the data
|
2023-01-20 22:02:44 +00:00
|
|
|
// in the response body. Optional path can be provided to differentiate requests.
|
2023-07-05 15:30:48 +00:00
|
|
|
func doHTTPServiceEchoes(t *testing.T, ip string, port int, path string, requestHeaders map[string]string, expectedResHeader map[string]string) {
|
2023-05-16 19:57:24 +00:00
|
|
|
client := cleanhttp.DefaultClient()
|
|
|
|
addr := fmt.Sprintf("%s:%d", ip, port)
|
2023-07-05 15:30:48 +00:00
|
|
|
doHTTPServiceEchoesWithClient(t, client, addr, path, requestHeaders, expectedResHeader)
|
2023-05-16 19:57:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func doHTTPServiceEchoesWithClient(
|
|
|
|
t *testing.T,
|
|
|
|
client *http.Client,
|
|
|
|
addr string,
|
|
|
|
path string,
|
2023-07-05 15:30:48 +00:00
|
|
|
requestHeaders map[string]string,
|
2023-05-16 19:57:24 +00:00
|
|
|
expectedResHeader map[string]string,
|
|
|
|
) {
|
2023-01-11 21:34:27 +00:00
|
|
|
const phrase = "hello"
|
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
failer := func() *retry.Timer {
|
|
|
|
return &retry.Timer{Timeout: defaultHTTPTimeout, Wait: defaultHTTPWait}
|
|
|
|
}
|
|
|
|
|
2023-05-16 19:57:24 +00:00
|
|
|
url := "http://" + addr
|
2022-11-01 19:03:23 +00:00
|
|
|
|
2023-01-20 22:02:44 +00:00
|
|
|
if path != "" {
|
|
|
|
url += "/" + path
|
|
|
|
}
|
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
retry.RunWith(failer(), t, func(r *retry.R) {
|
|
|
|
t.Logf("making call to %s", url)
|
2023-07-05 15:30:48 +00:00
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
reader := strings.NewReader(phrase)
|
2023-07-05 15:30:48 +00:00
|
|
|
req, err := http.NewRequest("POST", url, reader)
|
|
|
|
require.NoError(t, err, "could not construct request")
|
|
|
|
|
|
|
|
for k, v := range requestHeaders {
|
|
|
|
req.Header.Add(k, v)
|
|
|
|
|
|
|
|
if k == "Host" {
|
|
|
|
req.Host = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := client.Do(req)
|
2022-11-01 19:03:23 +00:00
|
|
|
if err != nil {
|
|
|
|
r.Fatal("could not make call to service ", url)
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
2023-05-16 19:57:24 +00:00
|
|
|
statusCode := res.StatusCode
|
|
|
|
t.Logf("...got response code %d", statusCode)
|
|
|
|
require.Equal(r, 200, statusCode)
|
|
|
|
|
2022-11-01 19:03:23 +00:00
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
r.Fatal("could not read response body ", url)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !strings.Contains(string(body), phrase) {
|
2023-02-03 15:20:22 +00:00
|
|
|
r.Fatal("received an incorrect response ", string(body))
|
2022-11-01 19:03:23 +00:00
|
|
|
}
|
2023-02-22 15:22:25 +00:00
|
|
|
|
|
|
|
for k, v := range expectedResHeader {
|
|
|
|
if headerValues, ok := res.Header[k]; !ok {
|
|
|
|
r.Fatal("expected header not found", k)
|
|
|
|
} else {
|
|
|
|
found := false
|
|
|
|
for _, value := range headerValues {
|
|
|
|
if value == v {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
r.Fatalf("header %s value not match want %s got %s ", k, v, headerValues)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-11-01 19:03:23 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-01-20 22:02:44 +00:00
|
|
|
// ServiceLogContains returns true if the service container has the target string in its logs
|
|
|
|
func ServiceLogContains(t *testing.T, service libservice.Service, target string) bool {
|
|
|
|
logs, err := service.GetLogs()
|
|
|
|
require.NoError(t, err)
|
|
|
|
return strings.Contains(logs, target)
|
2022-11-01 19:03:23 +00:00
|
|
|
}
|
2023-02-01 15:48:54 +00:00
|
|
|
|
2023-03-29 16:51:21 +00:00
|
|
|
// AssertFortioName is a convenience function for [AssertFortioNameWithClient], using a [cleanhttp.DefaultClient()]
|
|
|
|
func AssertFortioName(t *testing.T, urlbase string, name string, reqHost string) {
|
|
|
|
t.Helper()
|
|
|
|
client := cleanhttp.DefaultClient()
|
|
|
|
AssertFortioNameWithClient(t, urlbase, name, reqHost, client)
|
|
|
|
}
|
|
|
|
|
|
|
|
// AssertFortioNameWithClient asserts that the fortio service replying at urlbase/debug
|
2023-02-09 14:45:31 +00:00
|
|
|
// has a `FORTIO_NAME` env variable set. This validates that the client is sending
|
|
|
|
// traffic to the right envoy proxy.
|
|
|
|
//
|
2023-03-01 20:45:27 +00:00
|
|
|
// If reqHost is set, the Host field of the HTTP request will be set to its value.
|
|
|
|
//
|
2023-02-09 14:45:31 +00:00
|
|
|
// It retries with timeout defaultHTTPTimeout and wait defaultHTTPWait.
|
2023-03-29 16:51:21 +00:00
|
|
|
//
|
|
|
|
// client must be a custom http.Client
|
|
|
|
func AssertFortioNameWithClient(t *testing.T, urlbase string, name string, reqHost string, client *http.Client) {
|
2023-02-09 14:45:31 +00:00
|
|
|
t.Helper()
|
|
|
|
var fortioNameRE = regexp.MustCompile(("\nFORTIO_NAME=(.+)\n"))
|
|
|
|
retry.RunWith(&retry.Timer{Timeout: defaultHTTPTimeout, Wait: defaultHTTPWait}, t, func(r *retry.R) {
|
2023-02-13 19:09:12 +00:00
|
|
|
fullurl := fmt.Sprintf("%s/debug?env=dump", urlbase)
|
|
|
|
req, err := http.NewRequest("GET", fullurl, nil)
|
|
|
|
if err != nil {
|
2023-03-29 16:51:21 +00:00
|
|
|
r.Fatalf("could not build request to %q: %v", fullurl, err)
|
2023-02-13 19:09:12 +00:00
|
|
|
}
|
2023-03-01 20:45:27 +00:00
|
|
|
if reqHost != "" {
|
|
|
|
req.Host = reqHost
|
|
|
|
}
|
2023-02-13 19:09:12 +00:00
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
2023-03-29 16:51:21 +00:00
|
|
|
r.Fatalf("could not make request to %q: %v", fullurl, err)
|
2023-02-13 19:09:12 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
2023-03-29 16:51:21 +00:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
r.Fatalf("could not make request to %q: status %d", fullurl, resp.StatusCode)
|
|
|
|
}
|
2023-02-13 19:09:12 +00:00
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
2023-02-09 14:45:31 +00:00
|
|
|
if err != nil {
|
2023-03-29 16:51:21 +00:00
|
|
|
r.Fatalf("failed to read response body from %q: %v", fullurl, err)
|
2023-02-09 14:45:31 +00:00
|
|
|
}
|
2023-02-13 19:09:12 +00:00
|
|
|
|
|
|
|
m := fortioNameRE.FindStringSubmatch(string(body))
|
|
|
|
require.GreaterOrEqual(r, len(m), 2)
|
2023-05-16 19:57:24 +00:00
|
|
|
t.Logf("got response from server name %q expect %q", m[1], name)
|
2023-02-13 19:09:12 +00:00
|
|
|
assert.Equal(r, name, m[1])
|
2023-02-09 14:45:31 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-02-01 15:48:54 +00:00
|
|
|
// AssertContainerState validates service container status
|
|
|
|
func AssertContainerState(t *testing.T, service libservice.Service, state string) {
|
|
|
|
containerStatus, err := service.GetStatus()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, containerStatus, state, fmt.Sprintf("Expected: %s. Got %s", containerStatus, state))
|
|
|
|
}
|