mirror of
https://github.com/status-im/consul.git
synced 2025-01-09 13:26:07 +00:00
76b5de5039
* Upgrade Go to 1.21 * ci: detect Go backwards compatibility test version automatically For our submodules and other places we choose to test against previous Go versions, detect this version automatically from the current one rather than hard-coding it.
1262 lines
32 KiB
Go
1262 lines
32 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
crand "crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
)
|
|
|
|
type configCallback func(c *Config)
|
|
|
|
func makeClient(t *testing.T) (*Client, *testutil.TestServer) {
|
|
return makeClientWithConfig(t, nil, nil)
|
|
}
|
|
|
|
func makeClientWithoutConnect(t *testing.T) (*Client, *testutil.TestServer) {
|
|
return makeClientWithConfig(t, nil, func(serverConfig *testutil.TestServerConfig) {
|
|
serverConfig.Connect = nil
|
|
})
|
|
}
|
|
|
|
func makeACLClient(t *testing.T) (*Client, *testutil.TestServer) {
|
|
return makeClientWithConfig(t, func(clientConfig *Config) {
|
|
clientConfig.Token = "root"
|
|
}, func(serverConfig *testutil.TestServerConfig) {
|
|
serverConfig.PrimaryDatacenter = "dc1"
|
|
serverConfig.ACL.Tokens.InitialManagement = "root"
|
|
serverConfig.ACL.Tokens.Agent = "root"
|
|
serverConfig.ACL.Enabled = true
|
|
serverConfig.ACL.DefaultPolicy = "deny"
|
|
})
|
|
}
|
|
|
|
// Makes a client with Audit enabled, it requires ACLs
|
|
func makeAuditClient(t *testing.T) (*Client, *testutil.TestServer) {
|
|
return makeClientWithConfig(t, func(clientConfig *Config) {
|
|
clientConfig.Token = "root"
|
|
}, func(serverConfig *testutil.TestServerConfig) {
|
|
serverConfig.PrimaryDatacenter = "dc1"
|
|
serverConfig.ACL.Tokens.InitialManagement = "root"
|
|
serverConfig.ACL.Tokens.Agent = "root"
|
|
serverConfig.ACL.Enabled = true
|
|
serverConfig.ACL.DefaultPolicy = "deny"
|
|
serverConfig.Audit = &testutil.TestAuditConfig{
|
|
Enabled: true,
|
|
}
|
|
})
|
|
}
|
|
|
|
func makeNonBootstrappedACLClient(t *testing.T, defaultPolicy string) (*Client, *testutil.TestServer) {
|
|
return makeClientWithConfig(t,
|
|
func(clientConfig *Config) {
|
|
clientConfig.Token = ""
|
|
},
|
|
func(serverConfig *testutil.TestServerConfig) {
|
|
serverConfig.PrimaryDatacenter = "dc1"
|
|
serverConfig.ACL.Enabled = true
|
|
serverConfig.ACL.DefaultPolicy = defaultPolicy
|
|
serverConfig.Bootstrap = true
|
|
})
|
|
}
|
|
|
|
func makeClientWithCA(t *testing.T) (*Client, *testutil.TestServer) {
|
|
return makeClientWithConfig(t,
|
|
func(c *Config) {
|
|
c.TLSConfig = TLSConfig{
|
|
Address: "consul.test",
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
}
|
|
},
|
|
func(c *testutil.TestServerConfig) {
|
|
c.CAFile = "../test/client_certs/rootca.crt"
|
|
c.CertFile = "../test/client_certs/server.crt"
|
|
c.KeyFile = "../test/client_certs/server.key"
|
|
})
|
|
}
|
|
|
|
func makeClientWithConfig(
|
|
t *testing.T,
|
|
cb1 configCallback,
|
|
cb2 testutil.ServerConfigCallback) (*Client, *testutil.TestServer) {
|
|
// Skip test when -short flag provided; any tests that create a test
|
|
// server will take at least 100ms which is undesirable for -short
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
// Make client config
|
|
conf := DefaultConfig()
|
|
if cb1 != nil {
|
|
cb1(conf)
|
|
}
|
|
|
|
// Create server
|
|
var server *testutil.TestServer
|
|
var err error
|
|
retry.RunWith(retry.ThreeTimes(), t, func(r *retry.R) {
|
|
server, err = testutil.NewTestServerConfigT(r, cb2)
|
|
if err != nil {
|
|
r.Fatalf("Failed to start server: %v", err.Error())
|
|
}
|
|
})
|
|
if server.Config.Bootstrap {
|
|
server.WaitForLeader(t)
|
|
}
|
|
connectEnabled := server.Config.Connect["enabled"]
|
|
if enabled, ok := connectEnabled.(bool); ok && server.Config.Server && enabled {
|
|
server.WaitForActiveCARoot(t)
|
|
}
|
|
|
|
conf.Address = server.HTTPAddr
|
|
|
|
// Create client
|
|
client, err := NewClient(conf)
|
|
if err != nil {
|
|
server.Stop()
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
return client, server
|
|
}
|
|
|
|
func testKey() string {
|
|
buf := make([]byte, 16)
|
|
if _, err := crand.Read(buf); err != nil {
|
|
panic(fmt.Errorf("Failed to read random bytes: %v", err))
|
|
}
|
|
|
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
|
|
buf[0:4],
|
|
buf[4:6],
|
|
buf[6:8],
|
|
buf[8:10],
|
|
buf[10:16])
|
|
}
|
|
|
|
func testNodeServiceCheckRegistrations(t *testing.T, client *Client, datacenter string) {
|
|
t.Helper()
|
|
|
|
registrations := map[string]*CatalogRegistration{
|
|
"Node foo": {
|
|
Datacenter: datacenter,
|
|
Node: "foo",
|
|
ID: "e0155642-135d-4739-9853-a1ee6c9f945b",
|
|
Address: "127.0.0.2",
|
|
TaggedAddresses: map[string]string{
|
|
"lan": "127.0.0.2",
|
|
"wan": "198.18.0.2",
|
|
},
|
|
NodeMeta: map[string]string{
|
|
"env": "production",
|
|
"os": "linux",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "foo",
|
|
CheckID: "foo:alive",
|
|
Name: "foo-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "foo is alive and well",
|
|
},
|
|
&HealthCheck{
|
|
Node: "foo",
|
|
CheckID: "foo:ssh",
|
|
Name: "foo-remote-ssh",
|
|
Status: HealthPassing,
|
|
Notes: "foo has ssh access",
|
|
},
|
|
},
|
|
Locality: &Locality{Region: "us-west-1", Zone: "us-west-1a"},
|
|
},
|
|
"Service redis v1 on foo": {
|
|
Datacenter: datacenter,
|
|
Node: "foo",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "redisV1",
|
|
Service: "redis",
|
|
Tags: []string{"v1"},
|
|
Meta: map[string]string{"version": "1"},
|
|
Port: 1234,
|
|
Address: "198.18.1.2",
|
|
Locality: &Locality{Region: "us-west-1", Zone: "us-west-1a"},
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "foo",
|
|
CheckID: "foo:redisV1",
|
|
Name: "redis-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "redis v1 is alive and well",
|
|
ServiceID: "redisV1",
|
|
ServiceName: "redis",
|
|
},
|
|
},
|
|
},
|
|
"Service redis v2 on foo": {
|
|
Datacenter: datacenter,
|
|
Node: "foo",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "redisV2",
|
|
Service: "redis",
|
|
Tags: []string{"v2"},
|
|
Meta: map[string]string{"version": "2"},
|
|
Port: 1235,
|
|
Address: "198.18.1.2",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "foo",
|
|
CheckID: "foo:redisV2",
|
|
Name: "redis-v2-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "redis v2 is alive and well",
|
|
ServiceID: "redisV2",
|
|
ServiceName: "redis",
|
|
},
|
|
},
|
|
},
|
|
"Node bar": {
|
|
Datacenter: datacenter,
|
|
Node: "bar",
|
|
ID: "c6e7a976-8f4f-44b5-bdd3-631be7e8ecac",
|
|
Address: "127.0.0.3",
|
|
TaggedAddresses: map[string]string{
|
|
"lan": "127.0.0.3",
|
|
"wan": "198.18.0.3",
|
|
},
|
|
NodeMeta: map[string]string{
|
|
"env": "production",
|
|
"os": "windows",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "bar",
|
|
CheckID: "bar:alive",
|
|
Name: "bar-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "bar is alive and well",
|
|
},
|
|
},
|
|
},
|
|
"Service redis v1 on bar": {
|
|
Datacenter: datacenter,
|
|
Node: "bar",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "redisV1",
|
|
Service: "redis",
|
|
Tags: []string{"v1"},
|
|
Meta: map[string]string{"version": "1"},
|
|
Port: 1234,
|
|
Address: "198.18.1.3",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "bar",
|
|
CheckID: "bar:redisV1",
|
|
Name: "redis-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "redis v1 is alive and well",
|
|
ServiceID: "redisV1",
|
|
ServiceName: "redis",
|
|
},
|
|
},
|
|
},
|
|
"Service web v1 on bar": {
|
|
Datacenter: datacenter,
|
|
Node: "bar",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "webV1",
|
|
Service: "web",
|
|
Tags: []string{"v1", "connect"},
|
|
Meta: map[string]string{"version": "1", "connect": "enabled"},
|
|
Port: 443,
|
|
Address: "198.18.1.4",
|
|
Connect: &AgentServiceConnect{Native: true},
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "bar",
|
|
CheckID: "bar:web:v1",
|
|
Name: "web-v1-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "web connect v1 is alive and well",
|
|
ServiceID: "webV1",
|
|
ServiceName: "web",
|
|
},
|
|
},
|
|
},
|
|
"Node baz": {
|
|
Datacenter: datacenter,
|
|
Node: "baz",
|
|
ID: "12f96b27-a7b0-47bd-add7-044a2bfc7bfb",
|
|
Address: "127.0.0.4",
|
|
TaggedAddresses: map[string]string{
|
|
"lan": "127.0.0.4",
|
|
},
|
|
NodeMeta: map[string]string{
|
|
"env": "qa",
|
|
"os": "linux",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "baz",
|
|
CheckID: "baz:alive",
|
|
Name: "baz-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "baz is alive and well",
|
|
},
|
|
&HealthCheck{
|
|
Node: "baz",
|
|
CheckID: "baz:ssh",
|
|
Name: "baz-remote-ssh",
|
|
Status: HealthPassing,
|
|
Notes: "baz has ssh access",
|
|
},
|
|
},
|
|
},
|
|
"Service web v1 on baz": {
|
|
Datacenter: datacenter,
|
|
Node: "baz",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "webV1",
|
|
Service: "web",
|
|
Tags: []string{"v1", "connect"},
|
|
Meta: map[string]string{"version": "1", "connect": "enabled"},
|
|
Port: 443,
|
|
Address: "198.18.1.4",
|
|
Connect: &AgentServiceConnect{Native: true},
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "baz",
|
|
CheckID: "baz:web:v1",
|
|
Name: "web-v1-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "web connect v1 is alive and well",
|
|
ServiceID: "webV1",
|
|
ServiceName: "web",
|
|
},
|
|
},
|
|
},
|
|
"Service web v2 on baz": {
|
|
Datacenter: datacenter,
|
|
Node: "baz",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "webV2",
|
|
Service: "web",
|
|
Tags: []string{"v2", "connect"},
|
|
Meta: map[string]string{"version": "2", "connect": "enabled"},
|
|
Port: 8443,
|
|
Address: "198.18.1.4",
|
|
Connect: &AgentServiceConnect{Native: true},
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "baz",
|
|
CheckID: "baz:web:v2",
|
|
Name: "web-v2-liveness",
|
|
Status: HealthPassing,
|
|
Notes: "web connect v2 is alive and well",
|
|
ServiceID: "webV2",
|
|
ServiceName: "web",
|
|
},
|
|
},
|
|
},
|
|
"Service critical on baz": {
|
|
Datacenter: datacenter,
|
|
Node: "baz",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "criticalV2",
|
|
Service: "critical",
|
|
Tags: []string{"v2"},
|
|
Meta: map[string]string{"version": "2"},
|
|
Port: 8080,
|
|
Address: "198.18.1.4",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "baz",
|
|
CheckID: "baz:critical:v2",
|
|
Name: "critical-v2-liveness",
|
|
Status: HealthCritical,
|
|
Notes: "critical v2 is in the critical state",
|
|
ServiceID: "criticalV2",
|
|
ServiceName: "critical",
|
|
},
|
|
},
|
|
},
|
|
"Service warning on baz": {
|
|
Datacenter: datacenter,
|
|
Node: "baz",
|
|
SkipNodeUpdate: true,
|
|
Service: &AgentService{
|
|
Kind: ServiceKindTypical,
|
|
ID: "warningV2",
|
|
Service: "warning",
|
|
Tags: []string{"v2"},
|
|
Meta: map[string]string{"version": "2"},
|
|
Port: 8081,
|
|
Address: "198.18.1.4",
|
|
},
|
|
Checks: HealthChecks{
|
|
&HealthCheck{
|
|
Node: "baz",
|
|
CheckID: "baz:warning:v2",
|
|
Name: "warning-v2-liveness",
|
|
Status: HealthWarning,
|
|
Notes: "warning v2 is in the warning state",
|
|
ServiceID: "warningV2",
|
|
ServiceName: "warning",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
catalog := client.Catalog()
|
|
for name, reg := range registrations {
|
|
_, err := catalog.Register(reg, nil)
|
|
require.NoError(t, err, "Failed catalog registration for %q: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func TestAPI_DefaultConfig_env(t *testing.T) {
|
|
// t.Parallel() // DO NOT ENABLE !!!
|
|
// do not enable t.Parallel for this test since it modifies global state
|
|
// (environment) which has non-deterministic effects on the other tests
|
|
// which derive their default configuration from the environment
|
|
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
addr := "1.2.3.4:5678"
|
|
token := "abcd1234"
|
|
auth := "username:password"
|
|
|
|
os.Setenv(HTTPAddrEnvName, addr)
|
|
defer os.Setenv(HTTPAddrEnvName, "")
|
|
os.Setenv(HTTPTokenEnvName, token)
|
|
defer os.Setenv(HTTPTokenEnvName, "")
|
|
os.Setenv(HTTPAuthEnvName, auth)
|
|
defer os.Setenv(HTTPAuthEnvName, "")
|
|
os.Setenv(HTTPSSLEnvName, "1")
|
|
defer os.Setenv(HTTPSSLEnvName, "")
|
|
os.Setenv(HTTPCAFile, "ca.pem")
|
|
defer os.Setenv(HTTPCAFile, "")
|
|
os.Setenv(HTTPCAPath, "certs/")
|
|
defer os.Setenv(HTTPCAPath, "")
|
|
os.Setenv(HTTPClientCert, "client.crt")
|
|
defer os.Setenv(HTTPClientCert, "")
|
|
os.Setenv(HTTPClientKey, "client.key")
|
|
defer os.Setenv(HTTPClientKey, "")
|
|
os.Setenv(HTTPTLSServerName, "consul.test")
|
|
defer os.Setenv(HTTPTLSServerName, "")
|
|
os.Setenv(HTTPSSLVerifyEnvName, "0")
|
|
defer os.Setenv(HTTPSSLVerifyEnvName, "")
|
|
|
|
for i, config := range []*Config{
|
|
DefaultConfig(),
|
|
DefaultConfigWithLogger(testutil.Logger(t)),
|
|
DefaultNonPooledConfig(),
|
|
} {
|
|
if config.Address != addr {
|
|
t.Errorf("expected %q to be %q", config.Address, addr)
|
|
}
|
|
if config.Token != token {
|
|
t.Errorf("expected %q to be %q", config.Token, token)
|
|
}
|
|
if config.HttpAuth == nil {
|
|
t.Fatalf("expected HttpAuth to be enabled")
|
|
}
|
|
if config.HttpAuth.Username != "username" {
|
|
t.Errorf("expected %q to be %q", config.HttpAuth.Username, "username")
|
|
}
|
|
if config.HttpAuth.Password != "password" {
|
|
t.Errorf("expected %q to be %q", config.HttpAuth.Password, "password")
|
|
}
|
|
if config.Scheme != "https" {
|
|
t.Errorf("expected %q to be %q", config.Scheme, "https")
|
|
}
|
|
if config.TLSConfig.CAFile != "ca.pem" {
|
|
t.Errorf("expected %q to be %q", config.TLSConfig.CAFile, "ca.pem")
|
|
}
|
|
if config.TLSConfig.CAPath != "certs/" {
|
|
t.Errorf("expected %q to be %q", config.TLSConfig.CAPath, "certs/")
|
|
}
|
|
if config.TLSConfig.CertFile != "client.crt" {
|
|
t.Errorf("expected %q to be %q", config.TLSConfig.CertFile, "client.crt")
|
|
}
|
|
if config.TLSConfig.KeyFile != "client.key" {
|
|
t.Errorf("expected %q to be %q", config.TLSConfig.KeyFile, "client.key")
|
|
}
|
|
if config.TLSConfig.Address != "consul.test" {
|
|
t.Errorf("expected %q to be %q", config.TLSConfig.Address, "consul.test")
|
|
}
|
|
if !config.TLSConfig.InsecureSkipVerify {
|
|
t.Errorf("expected SSL verification to be off")
|
|
}
|
|
|
|
// Use keep alives as a check for whether pooling is on or off.
|
|
if pooled := i != 2; pooled {
|
|
if config.Transport.DisableKeepAlives != false {
|
|
t.Errorf("expected keep alives to be enabled")
|
|
}
|
|
} else {
|
|
if config.Transport.DisableKeepAlives != true {
|
|
t.Errorf("expected keep alives to be disabled")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPI_SetupTLSConfig(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
t.Parallel()
|
|
// A default config should result in a clean default client config.
|
|
tlsConfig := &TLSConfig{}
|
|
cc, err := SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected := &tls.Config{RootCAs: cc.RootCAs}
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: \n%v, \n%v", cc, expected)
|
|
}
|
|
|
|
// Try some address variations with and without ports.
|
|
tlsConfig.Address = "127.0.0.1"
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected.ServerName = "127.0.0.1"
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: %v", cc)
|
|
}
|
|
|
|
tlsConfig.Address = "127.0.0.1:80"
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected.ServerName = "127.0.0.1"
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: %v", cc)
|
|
}
|
|
|
|
tlsConfig.Address = "demo.consul.io:80"
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected.ServerName = "demo.consul.io"
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: %v", cc)
|
|
}
|
|
|
|
tlsConfig.Address = "[2001:db8:a0b:12f0::1]"
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected.ServerName = "[2001:db8:a0b:12f0::1]"
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: %v", cc)
|
|
}
|
|
|
|
tlsConfig.Address = "[2001:db8:a0b:12f0::1]:80"
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected.ServerName = "2001:db8:a0b:12f0::1"
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: %v", cc)
|
|
}
|
|
|
|
// Skip verification.
|
|
tlsConfig.InsecureSkipVerify = true
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expected.InsecureSkipVerify = true
|
|
if !reflect.DeepEqual(cc, expected) {
|
|
t.Fatalf("bad: %v", cc)
|
|
}
|
|
|
|
// Make a new config that hits all the file parsers.
|
|
tlsConfig = &TLSConfig{
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
}
|
|
cc, err = SetupTLSConfig(tlsConfig)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(cc.Certificates) != 1 {
|
|
t.Fatalf("missing certificate: %v", cc.Certificates)
|
|
}
|
|
if cc.RootCAs == nil {
|
|
t.Fatalf("didn't load root CAs")
|
|
}
|
|
|
|
// Use a directory to load the certs instead
|
|
cc, err = SetupTLSConfig(&TLSConfig{
|
|
CAPath: "../test/ca_path",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
expectedCaPoolByDir := getExpectedCaPoolByDir(t)
|
|
assertDeepEqual(t, expectedCaPoolByDir, cc.RootCAs, cmpCertPool)
|
|
|
|
// Load certs in-memory
|
|
certPEM, err := os.ReadFile("../test/hostname/Alice.crt")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
keyPEM, err := os.ReadFile("../test/hostname/Alice.key")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
caPEM, err := os.ReadFile("../test/hostname/CertAuth.crt")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
// Setup config with in-memory certs
|
|
cc, err = SetupTLSConfig(&TLSConfig{
|
|
CertPEM: certPEM,
|
|
KeyPEM: keyPEM,
|
|
CAPem: caPEM,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(cc.Certificates) != 1 {
|
|
t.Fatalf("missing certificate: %v", cc.Certificates)
|
|
}
|
|
if cc.RootCAs == nil {
|
|
t.Fatalf("didn't load root CAs")
|
|
}
|
|
}
|
|
|
|
func TestAPI_ClientTLSOptions(t *testing.T) {
|
|
t.Parallel()
|
|
// Start a server that verifies incoming HTTPS connections
|
|
_, srvVerify := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
|
|
conf.CAFile = "../test/client_certs/rootca.crt"
|
|
conf.CertFile = "../test/client_certs/server.crt"
|
|
conf.KeyFile = "../test/client_certs/server.key"
|
|
conf.VerifyIncomingHTTPS = true
|
|
})
|
|
defer srvVerify.Stop()
|
|
|
|
// Start a server without VerifyIncomingHTTPS
|
|
_, srvNoVerify := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
|
|
conf.CAFile = "../test/client_certs/rootca.crt"
|
|
conf.CertFile = "../test/client_certs/server.crt"
|
|
conf.KeyFile = "../test/client_certs/server.key"
|
|
conf.VerifyIncomingHTTPS = false
|
|
})
|
|
defer srvNoVerify.Stop()
|
|
|
|
// Client without a cert
|
|
t.Run("client without cert, validation", func(t *testing.T) {
|
|
client, err := NewClient(&Config{
|
|
Address: srvVerify.HTTPSAddr,
|
|
Scheme: "https",
|
|
TLSConfig: TLSConfig{
|
|
Address: "consul.test",
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Should fail
|
|
_, err = client.Agent().Self()
|
|
// Check for one of the possible cert error messages
|
|
// See https://cs.opensource.google/go/go/+/62a994837a57a7d0c58bb364b580a389488446c9
|
|
if err == nil || !(strings.Contains(err.Error(), "tls: bad certificate") ||
|
|
strings.Contains(err.Error(), "tls: certificate required")) {
|
|
t.Fatalf("expected tls certificate error, but got '%v'", err)
|
|
}
|
|
})
|
|
|
|
// Client with a valid cert
|
|
t.Run("client with cert, validation", func(t *testing.T) {
|
|
client, err := NewClient(&Config{
|
|
Address: srvVerify.HTTPSAddr,
|
|
Scheme: "https",
|
|
TLSConfig: TLSConfig{
|
|
Address: "consul.test",
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Should succeed
|
|
_, err = client.Agent().Self()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
// Client without a cert
|
|
t.Run("client without cert, no validation", func(t *testing.T) {
|
|
client, err := NewClient(&Config{
|
|
Address: srvNoVerify.HTTPSAddr,
|
|
Scheme: "https",
|
|
TLSConfig: TLSConfig{
|
|
Address: "consul.test",
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Should succeed
|
|
_, err = client.Agent().Self()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
// Client with a valid cert
|
|
t.Run("client with cert, no validation", func(t *testing.T) {
|
|
client, err := NewClient(&Config{
|
|
Address: srvNoVerify.HTTPSAddr,
|
|
Scheme: "https",
|
|
TLSConfig: TLSConfig{
|
|
Address: "consul.test",
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Should succeed
|
|
_, err = client.Agent().Self()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPI_SetQueryOptions(t *testing.T) {
|
|
t.Parallel()
|
|
c, s := makeClient(t)
|
|
defer s.Stop()
|
|
|
|
r := c.newRequest("GET", "/v1/kv/foo")
|
|
q := &QueryOptions{
|
|
Namespace: "operator",
|
|
Partition: "asdf",
|
|
Datacenter: "foo",
|
|
Peer: "dc10",
|
|
AllowStale: true,
|
|
RequireConsistent: true,
|
|
WaitIndex: 1000,
|
|
WaitTime: 100 * time.Second,
|
|
Token: "12345",
|
|
Near: "nodex",
|
|
LocalOnly: true,
|
|
}
|
|
r.setQueryOptions(q)
|
|
|
|
if r.params.Get("ns") != "operator" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("partition") != "asdf" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("peer") != "dc10" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("dc") != "foo" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if _, ok := r.params["stale"]; !ok {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if _, ok := r.params["consistent"]; !ok {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("index") != "1000" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("wait") != "100000ms" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.header.Get("X-Consul-Token") != "12345" {
|
|
t.Fatalf("bad: %v", r.header)
|
|
}
|
|
if r.params.Get("near") != "nodex" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("local-only") != "true" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
assert.Equal(t, "", r.header.Get("Cache-Control"))
|
|
|
|
r = c.newRequest("GET", "/v1/kv/foo")
|
|
q = &QueryOptions{
|
|
UseCache: true,
|
|
MaxAge: 30 * time.Second,
|
|
StaleIfError: 345678 * time.Millisecond, // Fractional seconds should be rounded
|
|
}
|
|
r.setQueryOptions(q)
|
|
|
|
_, ok := r.params["cached"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "max-age=30, stale-if-error=346", r.header.Get("Cache-Control"))
|
|
}
|
|
|
|
func TestAPI_SetWriteOptions(t *testing.T) {
|
|
t.Parallel()
|
|
c, s := makeClient(t)
|
|
defer s.Stop()
|
|
|
|
r := c.newRequest("GET", "/v1/kv/foo")
|
|
q := &WriteOptions{
|
|
Namespace: "operator",
|
|
Partition: "asdf",
|
|
Datacenter: "foo",
|
|
Token: "23456",
|
|
}
|
|
r.setWriteOptions(q)
|
|
if r.params.Get("ns") != "operator" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("partition") != "asdf" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.params.Get("dc") != "foo" {
|
|
t.Fatalf("bad: %v", r.params)
|
|
}
|
|
if r.header.Get("X-Consul-Token") != "23456" {
|
|
t.Fatalf("bad: %v", r.header)
|
|
}
|
|
}
|
|
|
|
func TestAPI_Headers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var request *http.Request
|
|
c, s := makeClientWithConfig(t, func(c *Config) {
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
transport.Proxy = func(r *http.Request) (*url.URL, error) {
|
|
// Keep track of the last request sent
|
|
request = r
|
|
return nil, nil
|
|
}
|
|
c.Transport = transport
|
|
}, nil)
|
|
defer s.Stop()
|
|
|
|
if len(c.Headers()) != 0 {
|
|
t.Fatalf("expected headers to be empty: %v", c.Headers())
|
|
}
|
|
|
|
c.AddHeader("Hello", "World")
|
|
r := c.newRequest("GET", "/v1/kv/foo")
|
|
|
|
if r.header.Get("Hello") != "World" {
|
|
t.Fatalf("Hello header not set : %v", r.header)
|
|
}
|
|
|
|
c.SetHeaders(http.Header{
|
|
"Auth": []string{"Token"},
|
|
})
|
|
|
|
r = c.newRequest("GET", "/v1/kv/foo")
|
|
if r.header.Get("Hello") != "" {
|
|
t.Fatalf("Hello header should not be set: %v", r.header)
|
|
}
|
|
|
|
if r.header.Get("Auth") != "Token" {
|
|
t.Fatalf("Auth header not set: %v", r.header)
|
|
}
|
|
|
|
kv := c.KV()
|
|
_, err := kv.Put(&KVPair{Key: "test-headers", Value: []byte("foo")}, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "application/octet-stream", request.Header.Get("Content-Type"))
|
|
|
|
_, _, err = kv.Get("test-headers", nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", request.Header.Get("Content-Type"))
|
|
|
|
_, err = kv.Delete("test-headers", nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", request.Header.Get("Content-Type"))
|
|
|
|
err = c.Snapshot().Restore(nil, strings.NewReader("foo"))
|
|
require.Error(t, err)
|
|
require.Equal(t, "application/octet-stream", request.Header.Get("Content-Type"))
|
|
|
|
_, _, err = c.Event().Fire(&UserEvent{
|
|
Name: "test",
|
|
Payload: []byte("foo"),
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "application/octet-stream", request.Header.Get("Content-Type"))
|
|
}
|
|
|
|
func TestAPI_Deprecated(t *testing.T) {
|
|
t.Parallel()
|
|
c, s := makeClientWithConfig(t, func(c *Config) {
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
c.Transport = transport
|
|
}, nil)
|
|
defer s.Stop()
|
|
// Rules translation functionality was completely removed in Consul 1.15.
|
|
_, err := c.ACL().RulesTranslate(strings.NewReader(`
|
|
agent "" {
|
|
policy = "read"
|
|
}
|
|
`))
|
|
require.Error(t, err)
|
|
_, err = c.ACL().RulesTranslateToken("")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAPI_RequestToHTTP(t *testing.T) {
|
|
t.Parallel()
|
|
c, s := makeClient(t)
|
|
defer s.Stop()
|
|
|
|
r := c.newRequest("DELETE", "/v1/kv/foo")
|
|
q := &QueryOptions{
|
|
Datacenter: "foo",
|
|
}
|
|
r.setQueryOptions(q)
|
|
req, err := r.toHTTP()
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if req.Method != "DELETE" {
|
|
t.Fatalf("bad: %v", req)
|
|
}
|
|
if req.URL.RequestURI() != "/v1/kv/foo?dc=foo" {
|
|
t.Fatalf("bad: %v", req)
|
|
}
|
|
}
|
|
|
|
func TestAPI_ParseQueryMeta(t *testing.T) {
|
|
t.Parallel()
|
|
resp := &http.Response{
|
|
Header: make(map[string][]string),
|
|
}
|
|
resp.Header.Set("X-Consul-Index", "12345")
|
|
resp.Header.Set("X-Consul-LastContact", "80")
|
|
resp.Header.Set("X-Consul-KnownLeader", "true")
|
|
resp.Header.Set("X-Consul-Translate-Addresses", "true")
|
|
resp.Header.Set("X-Consul-Default-ACL-Policy", "deny")
|
|
resp.Header.Set("X-Consul-Results-Filtered-By-ACLs", "true")
|
|
|
|
qm := &QueryMeta{}
|
|
if err := parseQueryMeta(resp, qm); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if qm.LastIndex != 12345 {
|
|
t.Fatalf("Bad: %v", qm)
|
|
}
|
|
if qm.LastContact != 80*time.Millisecond {
|
|
t.Fatalf("Bad: %v", qm)
|
|
}
|
|
if !qm.KnownLeader {
|
|
t.Fatalf("Bad: %v", qm)
|
|
}
|
|
if !qm.AddressTranslationEnabled {
|
|
t.Fatalf("Bad: %v", qm)
|
|
}
|
|
if qm.DefaultACLPolicy != "deny" {
|
|
t.Fatalf("Bad: %v", qm)
|
|
}
|
|
if !qm.ResultsFilteredByACLs {
|
|
t.Fatalf("Bad: %v", qm)
|
|
}
|
|
}
|
|
|
|
func TestAPI_UnixSocket(t *testing.T) {
|
|
t.Parallel()
|
|
if runtime.GOOS == "windows" {
|
|
t.SkipNow()
|
|
}
|
|
|
|
tempDir := testutil.TempDir(t, "consul")
|
|
socket := filepath.Join(tempDir, "test.sock")
|
|
|
|
c, s := makeClientWithConfig(t, func(c *Config) {
|
|
c.Address = "unix://" + socket
|
|
}, func(c *testutil.TestServerConfig) {
|
|
c.Addresses = &testutil.TestAddressConfig{
|
|
HTTP: "unix://" + socket,
|
|
}
|
|
})
|
|
defer s.Stop()
|
|
|
|
agent := c.Agent()
|
|
|
|
info, err := agent.Self()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if info["Config"]["NodeName"].(string) == "" {
|
|
t.Fatalf("bad: %v", info)
|
|
}
|
|
}
|
|
|
|
func TestAPI_durToMsec(t *testing.T) {
|
|
t.Parallel()
|
|
if ms := durToMsec(0); ms != "0ms" {
|
|
t.Fatalf("bad: %s", ms)
|
|
}
|
|
|
|
if ms := durToMsec(time.Millisecond); ms != "1ms" {
|
|
t.Fatalf("bad: %s", ms)
|
|
}
|
|
|
|
if ms := durToMsec(time.Microsecond); ms != "1ms" {
|
|
t.Fatalf("bad: %s", ms)
|
|
}
|
|
|
|
if ms := durToMsec(5 * time.Millisecond); ms != "5ms" {
|
|
t.Fatalf("bad: %s", ms)
|
|
}
|
|
}
|
|
|
|
func TestAPI_IsRetryableError(t *testing.T) {
|
|
t.Parallel()
|
|
if IsRetryableError(nil) {
|
|
t.Fatal("should not be a retryable error")
|
|
}
|
|
|
|
if IsRetryableError(fmt.Errorf("not the error you are looking for")) {
|
|
t.Fatal("should not be a retryable error")
|
|
}
|
|
|
|
if !IsRetryableError(fmt.Errorf(serverError)) {
|
|
t.Fatal("should be a retryable error")
|
|
}
|
|
|
|
if !IsRetryableError(&net.OpError{Err: fmt.Errorf("network conn error")}) {
|
|
t.Fatal("should be a retryable error")
|
|
}
|
|
}
|
|
|
|
func TestAPI_GenerateEnv(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := &Config{
|
|
Address: "127.0.0.1:8500",
|
|
Token: "test",
|
|
TokenFile: "test.file",
|
|
Scheme: "http",
|
|
TLSConfig: TLSConfig{
|
|
CAFile: "",
|
|
CAPath: "",
|
|
CertFile: "",
|
|
KeyFile: "",
|
|
Address: "",
|
|
InsecureSkipVerify: true,
|
|
},
|
|
}
|
|
|
|
expected := []string{
|
|
"CONSUL_HTTP_ADDR=127.0.0.1:8500",
|
|
"CONSUL_HTTP_TOKEN=test",
|
|
"CONSUL_HTTP_TOKEN_FILE=test.file",
|
|
"CONSUL_HTTP_SSL=false",
|
|
"CONSUL_CACERT=",
|
|
"CONSUL_CAPATH=",
|
|
"CONSUL_CLIENT_CERT=",
|
|
"CONSUL_CLIENT_KEY=",
|
|
"CONSUL_TLS_SERVER_NAME=",
|
|
"CONSUL_HTTP_SSL_VERIFY=false",
|
|
"CONSUL_HTTP_AUTH=",
|
|
}
|
|
|
|
require.Equal(t, expected, c.GenerateEnv())
|
|
}
|
|
|
|
func TestAPI_GenerateEnvHTTPS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := &Config{
|
|
Address: "127.0.0.1:8500",
|
|
Token: "test",
|
|
TokenFile: "test.file",
|
|
Scheme: "https",
|
|
TLSConfig: TLSConfig{
|
|
CAFile: "/var/consul/ca.crt",
|
|
CAPath: "/var/consul/ca.dir",
|
|
CertFile: "/var/consul/server.crt",
|
|
KeyFile: "/var/consul/ssl/server.key",
|
|
Address: "127.0.0.1:8500",
|
|
InsecureSkipVerify: false,
|
|
},
|
|
HttpAuth: &HttpBasicAuth{
|
|
Username: "user",
|
|
Password: "password",
|
|
},
|
|
}
|
|
|
|
expected := []string{
|
|
"CONSUL_HTTP_ADDR=127.0.0.1:8500",
|
|
"CONSUL_HTTP_TOKEN=test",
|
|
"CONSUL_HTTP_TOKEN_FILE=test.file",
|
|
"CONSUL_HTTP_SSL=true",
|
|
"CONSUL_CACERT=/var/consul/ca.crt",
|
|
"CONSUL_CAPATH=/var/consul/ca.dir",
|
|
"CONSUL_CLIENT_CERT=/var/consul/server.crt",
|
|
"CONSUL_CLIENT_KEY=/var/consul/ssl/server.key",
|
|
"CONSUL_TLS_SERVER_NAME=127.0.0.1:8500",
|
|
"CONSUL_HTTP_SSL_VERIFY=true",
|
|
"CONSUL_HTTP_AUTH=user:password",
|
|
}
|
|
|
|
require.Equal(t, expected, c.GenerateEnv())
|
|
}
|
|
|
|
// TestAPI_PrefixPath() validates that Config.Address is split into
|
|
// Config.Address and Config.PathPrefix as expected. If we want to add end to
|
|
// end testing in the future this will require configuring and running an
|
|
// API gateway / reverse proxy (e.g. nginx)
|
|
func TestAPI_PrefixPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
addr string
|
|
expectAddr string
|
|
expectPrefix string
|
|
}{
|
|
{
|
|
name: "with http and prefix",
|
|
addr: "http://reverse.proxy.com/consul/path/prefix",
|
|
expectAddr: "reverse.proxy.com",
|
|
expectPrefix: "/consul/path/prefix",
|
|
},
|
|
{
|
|
name: "with https and prefix",
|
|
addr: "https://reverse.proxy.com/consul/path/prefix",
|
|
expectAddr: "reverse.proxy.com",
|
|
expectPrefix: "/consul/path/prefix",
|
|
},
|
|
{
|
|
name: "with http and no prefix",
|
|
addr: "http://localhost",
|
|
expectAddr: "localhost",
|
|
expectPrefix: "",
|
|
},
|
|
{
|
|
name: "with https and no prefix",
|
|
addr: "https://localhost",
|
|
expectAddr: "localhost",
|
|
expectPrefix: "",
|
|
},
|
|
{
|
|
name: "no scheme and no prefix",
|
|
addr: "localhost",
|
|
expectAddr: "localhost",
|
|
expectPrefix: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := &Config{Address: tc.addr}
|
|
client, err := NewClient(c)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expectAddr, client.config.Address)
|
|
require.Equal(t, tc.expectPrefix, client.config.PathPrefix)
|
|
})
|
|
}
|
|
}
|
|
|
|
func getExpectedCaPoolByDir(t *testing.T) *x509.CertPool {
|
|
pool := x509.NewCertPool()
|
|
entries, err := os.ReadDir("../test/ca_path")
|
|
require.NoError(t, err)
|
|
|
|
for _, entry := range entries {
|
|
filename := path.Join("../test/ca_path", entry.Name())
|
|
|
|
data, err := os.ReadFile(filename)
|
|
require.NoError(t, err)
|
|
|
|
if !pool.AppendCertsFromPEM(data) {
|
|
t.Fatalf("could not add test ca %s to pool", filename)
|
|
}
|
|
}
|
|
|
|
return pool
|
|
}
|
|
|
|
// lazyCerts has a func field which can't be compared.
|
|
var cmpCertPool = cmp.Options{
|
|
cmpopts.IgnoreFields(x509.CertPool{}, "lazyCerts"),
|
|
cmp.AllowUnexported(x509.CertPool{}),
|
|
}
|
|
|
|
func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) {
|
|
t.Helper()
|
|
if diff := cmp.Diff(x, y, opts...); diff != "" {
|
|
t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff)
|
|
}
|
|
}
|