package agent

import (
	"bytes"
	"encoding/base64"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/consul/lib"
)

func TestConfigEncryptBytes(t *testing.T) {
	// Test with some input
	src := []byte("abc")
	c := &Config{
		EncryptKey: base64.StdEncoding.EncodeToString(src),
	}

	result, err := c.EncryptBytes()
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !bytes.Equal(src, result) {
		t.Fatalf("bad: %#v", result)
	}

	// Test with no input
	c = &Config{}
	result, err = c.EncryptBytes()
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(result) > 0 {
		t.Fatalf("bad: %#v", result)
	}
}

func TestDecodeConfig(t *testing.T) {
	// Basics
	input := `{"data_dir": "/tmp/", "log_level": "debug"}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.DataDir != "/tmp/" {
		t.Fatalf("bad: %#v", config)
	}

	if config.LogLevel != "debug" {
		t.Fatalf("bad: %#v", config)
	}

	// Without a protocol
	input = `{"node_id": "bar", "node_name": "foo", "datacenter": "dc2"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.NodeName != "foo" {
		t.Fatalf("bad: %#v", config)
	}

	if config.NodeID != "bar" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Datacenter != "dc2" {
		t.Fatalf("bad: %#v", config)
	}

	if config.SkipLeaveOnInt != nil {
		t.Fatalf("bad: expected nil SkipLeaveOnInt")
	}

	if config.LeaveOnTerm != nil {
		t.Fatalf("bad: expected nil LeaveOnTerm")
	}

	// Server bootstrap
	input = `{"server": true, "bootstrap": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.Server {
		t.Fatalf("bad: %#v", config)
	}

	if !config.Bootstrap {
		t.Fatalf("bad: %#v", config)
	}

	// Expect bootstrap
	input = `{"server": true, "bootstrap_expect": 3}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.Server {
		t.Fatalf("bad: %#v", config)
	}

	if config.BootstrapExpect != 3 {
		t.Fatalf("bad: %#v", config)
	}

	// DNS setup
	input = `{"ports": {"dns": 8500}, "recursors": ["8.8.8.8","8.8.4.4"], "recursor":"127.0.0.1", "domain": "foobar"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.Ports.DNS != 8500 {
		t.Fatalf("bad: %#v", config)
	}

	if len(config.DNSRecursors) != 3 {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSRecursors[0] != "8.8.8.8" {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSRecursors[1] != "8.8.4.4" {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSRecursors[2] != "127.0.0.1" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Domain != "foobar" {
		t.Fatalf("bad: %#v", config)
	}

	// RPC configs
	input = `{"ports": {"http": 1234, "https": 1243}, "client_addr": "0.0.0.0"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.ClientAddr != "0.0.0.0" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Ports.HTTP != 1234 {
		t.Fatalf("bad: %#v", config)
	}

	if config.Ports.HTTPS != 1243 {
		t.Fatalf("bad: %#v", config)
	}

	// Deprecated RPC configs - TODO: remove this in a future release
	input = `{"ports": {"rpc": 1234}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.Ports.RPC != 1234 {
		t.Fatalf("bad: %#v", config)
	}

	// Serf configs
	input = `{"ports": {"serf_lan": 1000, "serf_wan": 2000}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.Ports.SerfLan != 1000 {
		t.Fatalf("bad: %#v", config)
	}

	if config.Ports.SerfWan != 2000 {
		t.Fatalf("bad: %#v", config)
	}

	// Server addrs
	input = `{"ports": {"server": 8000}, "bind_addr": "127.0.0.2", "advertise_addr": "127.0.0.3", "serf_lan_bind": "127.0.0.4", "serf_wan_bind": "52.54.55.56"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))

	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.BindAddr != "127.0.0.2" {
		t.Fatalf("bad: %#v", config)
	}

	if config.SerfWanBindAddr != "52.54.55.56" {
		t.Fatalf("bad: %#v", config)
	}

	if config.SerfLanBindAddr != "127.0.0.4" {
		t.Fatalf("bad: %#v", config)
	}

	if config.AdvertiseAddr != "127.0.0.3" {
		t.Fatalf("bad: %#v", config)
	}

	if config.AdvertiseAddrWan != "" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Ports.Server != 8000 {
		t.Fatalf("bad: %#v", config)
	}

	// Advertise address for wan
	input = `{"advertise_addr_wan": "127.0.0.5"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.AdvertiseAddr != "" {
		t.Fatalf("bad: %#v", config)
	}
	if config.AdvertiseAddrWan != "127.0.0.5" {
		t.Fatalf("bad: %#v", config)
	}

	// Advertise addresses for serflan
	input = `{"advertise_addrs": {"serf_lan": "127.0.0.5:1234"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.AdvertiseAddrs.SerfLanRaw != "127.0.0.5:1234" {
		t.Fatalf("bad: %#v", config)
	}
	if config.AdvertiseAddrs.SerfLan.String() != "127.0.0.5:1234" {
		t.Fatalf("bad: %#v", config)
	}

	// Advertise addresses for serfwan
	input = `{"advertise_addrs": {"serf_wan": "127.0.0.5:1234"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.AdvertiseAddrs.SerfWanRaw != "127.0.0.5:1234" {
		t.Fatalf("bad: %#v", config)
	}
	if config.AdvertiseAddrs.SerfWan.String() != "127.0.0.5:1234" {
		t.Fatalf("bad: %#v", config)
	}

	// Advertise addresses for rpc
	input = `{"advertise_addrs": {"rpc": "127.0.0.5:1234"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.AdvertiseAddrs.RPCRaw != "127.0.0.5:1234" {
		t.Fatalf("bad: %#v", config)
	}
	if config.AdvertiseAddrs.RPC.String() != "127.0.0.5:1234" {
		t.Fatalf("bad: %#v", config)
	}

	// WAN address translation disabled by default
	config, err = DecodeConfig(bytes.NewReader([]byte(`{}`)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.TranslateWanAddrs != false {
		t.Fatalf("bad: %#v", config)
	}

	// WAN address translation
	input = `{"translate_wan_addrs": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.TranslateWanAddrs != true {
		t.Fatalf("bad: %#v", config)
	}

	// raft protocol
	input = `{"raft_protocol": 3}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RaftProtocol != 3 {
		t.Fatalf("bad: %#v", config)
	}

	// Node metadata fields
	input = `{"node_meta": {"thing1": "1", "thing2": "2"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if v, ok := config.Meta["thing1"]; !ok || v != "1" {
		t.Fatalf("bad: %#v", config)
	}
	if v, ok := config.Meta["thing2"]; !ok || v != "2" {
		t.Fatalf("bad: %#v", config)
	}

	// leave_on_terminate
	input = `{"leave_on_terminate": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if *config.LeaveOnTerm != true {
		t.Fatalf("bad: %#v", config)
	}

	// skip_leave_on_interrupt
	input = `{"skip_leave_on_interrupt": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if *config.SkipLeaveOnInt != true {
		t.Fatalf("bad: %#v", config)
	}

	// enable_debug
	input = `{"enable_debug": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.EnableDebug != true {
		t.Fatalf("bad: %#v", config)
	}

	// TLS
	input = `{"verify_incoming": true, "verify_outgoing": true, "verify_server_hostname": true, "tls_min_version": "tls12"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.VerifyIncoming != true {
		t.Fatalf("bad: %#v", config)
	}

	if config.VerifyOutgoing != true {
		t.Fatalf("bad: %#v", config)
	}

	if config.VerifyServerHostname != true {
		t.Fatalf("bad: %#v", config)
	}

	if config.TLSMinVersion != "tls12" {
		t.Fatalf("bad: %#v", config)
	}

	// TLS keys
	input = `{"ca_file": "my/ca/file", "cert_file": "my.cert", "key_file": "key.pem", "server_name": "example.com"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.CAFile != "my/ca/file" {
		t.Fatalf("bad: %#v", config)
	}
	if config.CertFile != "my.cert" {
		t.Fatalf("bad: %#v", config)
	}
	if config.KeyFile != "key.pem" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ServerName != "example.com" {
		t.Fatalf("bad: %#v", config)
	}

	// Start join
	input = `{"start_join": ["1.1.1.1", "2.2.2.2"]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.StartJoin) != 2 {
		t.Fatalf("bad: %#v", config)
	}
	if config.StartJoin[0] != "1.1.1.1" {
		t.Fatalf("bad: %#v", config)
	}
	if config.StartJoin[1] != "2.2.2.2" {
		t.Fatalf("bad: %#v", config)
	}

	// Start Join wan
	input = `{"start_join_wan": ["1.1.1.1", "2.2.2.2"]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.StartJoinWan) != 2 {
		t.Fatalf("bad: %#v", config)
	}
	if config.StartJoinWan[0] != "1.1.1.1" {
		t.Fatalf("bad: %#v", config)
	}
	if config.StartJoinWan[1] != "2.2.2.2" {
		t.Fatalf("bad: %#v", config)
	}

	// Retry join
	input = `{"retry_join": ["1.1.1.1", "2.2.2.2"]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.RetryJoin) != 2 {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoin[0] != "1.1.1.1" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoin[1] != "2.2.2.2" {
		t.Fatalf("bad: %#v", config)
	}

	// Retry interval
	input = `{"retry_interval": "10s"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RetryIntervalRaw != "10s" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryInterval.String() != "10s" {
		t.Fatalf("bad: %#v", config)
	}

	// Retry Max
	input = `{"retry_max": 3}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RetryMaxAttempts != 3 {
		t.Fatalf("bad: %#v", config)
	}

	// Retry Join wan
	input = `{"retry_join_wan": ["1.1.1.1", "2.2.2.2"]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.RetryJoinWan) != 2 {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinWan[0] != "1.1.1.1" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinWan[1] != "2.2.2.2" {
		t.Fatalf("bad: %#v", config)
	}

	// Retry Interval wan
	input = `{"retry_interval_wan": "10s"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RetryIntervalWanRaw != "10s" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryIntervalWan.String() != "10s" {
		t.Fatalf("bad: %#v", config)
	}

	// Retry Max wan
	input = `{"retry_max_wan": 3}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RetryMaxAttemptsWan != 3 {
		t.Fatalf("bad: %#v", config)
	}

	// Reconnect timeout LAN and WAN
	input = `{"reconnect_timeout": "8h", "reconnect_timeout_wan": "10h"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.ReconnectTimeoutLanRaw != "8h" ||
		config.ReconnectTimeoutLan.String() != "8h0m0s" ||
		config.ReconnectTimeoutWanRaw != "10h" ||
		config.ReconnectTimeoutWan.String() != "10h0m0s" {
		t.Fatalf("bad: %#v", config)
	}
	input = `{"reconnect_timeout": "7h"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err == nil {
		t.Fatalf("decode should have failed")
	}
	input = `{"reconnect_timeout_wan": "7h"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err == nil {
		t.Fatalf("decode should have failed")
	}

	// Static UI server
	input = `{"ui": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.EnableUi {
		t.Fatalf("bad: %#v", config)
	}

	// UI Dir
	input = `{"ui_dir": "/opt/consul-ui"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.UiDir != "/opt/consul-ui" {
		t.Fatalf("bad: %#v", config)
	}

	// Pid File
	input = `{"pid_file": "/tmp/consul/pid"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.PidFile != "/tmp/consul/pid" {
		t.Fatalf("bad: %#v", config)
	}

	// Syslog
	input = `{"enable_syslog": true, "syslog_facility": "LOCAL4"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.EnableSyslog {
		t.Fatalf("bad: %#v", config)
	}
	if config.SyslogFacility != "LOCAL4" {
		t.Fatalf("bad: %#v", config)
	}

	// Rejoin
	input = `{"rejoin_after_leave": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.RejoinAfterLeave {
		t.Fatalf("bad: %#v", config)
	}

	// DNS node ttl, max stale
	input = `{"dns_config": {"allow_stale": false, "enable_truncate": false, "max_stale": "15s", "node_ttl": "5s", "only_passing": true, "udp_answer_limit": 6, "recursor_timeout": "7s"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if *config.DNSConfig.AllowStale {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.EnableTruncate {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.MaxStale != 15*time.Second {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.NodeTTL != 5*time.Second {
		t.Fatalf("bad: %#v", config)
	}
	if !config.DNSConfig.OnlyPassing {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.UDPAnswerLimit != 6 {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.RecursorTimeout != 7*time.Second {
		t.Fatalf("bad: %#v", config)
	}

	// DNS service ttl
	input = `{"dns_config": {"service_ttl": {"*": "1s", "api": "10s", "web": "30s"}}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.DNSConfig.ServiceTTL["*"] != time.Second {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.ServiceTTL["api"] != 10*time.Second {
		t.Fatalf("bad: %#v", config)
	}
	if config.DNSConfig.ServiceTTL["web"] != 30*time.Second {
		t.Fatalf("bad: %#v", config)
	}

	// DNS enable truncate
	input = `{"dns_config": {"enable_truncate": true}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.DNSConfig.EnableTruncate {
		t.Fatalf("bad: %#v", config)
	}

	// DNS only passing
	input = `{"dns_config": {"only_passing": true}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.DNSConfig.OnlyPassing {
		t.Fatalf("bad: %#v", config)
	}

	// DNS disable compression
	input = `{"dns_config": {"disable_compression": true}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.DNSConfig.DisableCompression {
		t.Fatalf("bad: %#v", config)
	}

	// CheckUpdateInterval
	input = `{"check_update_interval": "10m"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.CheckUpdateInterval != 10*time.Minute {
		t.Fatalf("bad: %#v", config)
	}

	// ACLs
	input = `{"acl_token": "1111", "acl_agent_master_token": "2222",
	"acl_agent_token": "3333", "acl_datacenter": "dc2",
	"acl_ttl": "60s", "acl_down_policy": "deny",
	"acl_default_policy": "deny", "acl_master_token": "2345",
	"acl_replication_token": "8675309"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.ACLToken != "1111" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLAgentMasterToken != "2222" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLAgentToken != "3333" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLMasterToken != "2345" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLDatacenter != "dc2" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLTTL != 60*time.Second {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLDownPolicy != "deny" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLDefaultPolicy != "deny" {
		t.Fatalf("bad: %#v", config)
	}
	if config.ACLReplicationToken != "8675309" {
		t.Fatalf("bad: %#v", config)
	}

	// ACL token precedence.
	input = `{}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if token := config.GetTokenForAgent(); token != "" {
		t.Fatalf("bad: %s", token)
	}
	input = `{"acl_token": "hello"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if token := config.GetTokenForAgent(); token != "hello" {
		t.Fatalf("bad: %s", token)
	}
	input = `{"acl_agent_token": "world", "acl_token": "hello"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if token := config.GetTokenForAgent(); token != "world" {
		t.Fatalf("bad: %s", token)
	}

	// ACL flag for Consul version 0.8 features (broken out since we will
	// eventually remove this).
	config = DefaultConfig()
	if *config.ACLEnforceVersion8 != true {
		t.Fatalf("bad: %#v", config)
	}

	input = `{"acl_enforce_version_8": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if *config.ACLEnforceVersion8 != true {
		t.Fatalf("bad: %#v", config)
	}

	// Watches
	input = `{"watches": [{"type":"keyprefix", "prefix":"foo/", "handler":"foobar"}]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.Watches) != 1 {
		t.Fatalf("bad: %#v", config)
	}

	out := config.Watches[0]
	exp := map[string]interface{}{
		"type":    "keyprefix",
		"prefix":  "foo/",
		"handler": "foobar",
	}
	if !reflect.DeepEqual(out, exp) {
		t.Fatalf("bad: %#v", config)
	}

	// remote exec
	input = `{"disable_remote_exec": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.DisableRemoteExec {
		t.Fatalf("bad: %#v", config)
	}

	// stats(d|ite) exec
	input = `{"statsite_addr": "127.0.0.1:7250", "statsd_addr": "127.0.0.1:7251"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.Telemetry.StatsiteAddr != "127.0.0.1:7250" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.StatsdAddr != "127.0.0.1:7251" {
		t.Fatalf("bad: %#v", config)
	}

	// dogstatsd
	input = `{"dogstatsd_addr": "127.0.0.1:7254", "dogstatsd_tags":["tag_1:val_1", "tag_2:val_2"]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Telemetry.DogStatsdAddr != "127.0.0.1:7254" {
		t.Fatalf("bad: %#v", config)
	}

	if len(config.Telemetry.DogStatsdTags) != 2 {
		t.Fatalf("bad: %#v", config)
	}

	if config.Telemetry.DogStatsdTags[0] != "tag_1:val_1" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Telemetry.DogStatsdTags[1] != "tag_2:val_2" {
		t.Fatalf("bad: %#v", config)
	}

	// Statsite prefix
	input = `{"statsite_prefix": "my_prefix"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Telemetry.StatsitePrefix != "my_prefix" {
		t.Fatalf("bad: %#v", config)
	}

	// Circonus settings
	input = `{"telemetry": {"circonus_api_token": "12345678-1234-1234-12345678", "circonus_api_app": "testApp",
    "circonus_api_url": "https://api.host.foo/v2", "circonus_submission_interval": "15s",
    "circonus_submission_url": "https://submit.host.bar:123/one/two/three",
	"circonus_check_id": "12345", "circonus_check_force_metric_activation": "true",
    "circonus_check_instance_id": "a:b", "circonus_check_search_tag": "c:d",
    "circonus_check_display_name": "node1:consul", "circonus_check_tags": "cat1:tag1,cat2:tag2",
    "circonus_broker_id": "6789", "circonus_broker_select_tag": "e:f"} }`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Telemetry.CirconusAPIToken != "12345678-1234-1234-12345678" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusAPIApp != "testApp" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusAPIURL != "https://api.host.foo/v2" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusSubmissionInterval != "15s" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckSubmissionURL != "https://submit.host.bar:123/one/two/three" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckID != "12345" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckForceMetricActivation != "true" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckInstanceID != "a:b" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckSearchTag != "c:d" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckDisplayName != "node1:consul" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusCheckTags != "cat1:tag1,cat2:tag2" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusBrokerID != "6789" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.CirconusBrokerSelectTag != "e:f" {
		t.Fatalf("bad: %#v", config)
	}

	// New telemetry
	input = `{"telemetry": { "statsite_prefix": "my_prefix", "statsite_address": "127.0.0.1:7250", "statsd_address":"127.0.0.1:7251", "disable_hostname": true, "dogstatsd_addr": "1.1.1.1:111", "dogstatsd_tags": [ "tag_1:val_1" ] } }`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Telemetry.StatsitePrefix != "my_prefix" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.StatsiteAddr != "127.0.0.1:7250" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.StatsdAddr != "127.0.0.1:7251" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.DisableHostname != true {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.DogStatsdAddr != "1.1.1.1:111" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Telemetry.DogStatsdTags[0] != "tag_1:val_1" {
		t.Fatalf("bad: %#v", config)
	}

	// Address overrides
	input = `{"addresses": {"dns": "0.0.0.0", "http": "127.0.0.1", "https": "127.0.0.1"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.Addresses.DNS != "0.0.0.0" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Addresses.HTTP != "127.0.0.1" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Addresses.HTTPS != "127.0.0.1" {
		t.Fatalf("bad: %#v", config)
	}

	// RPC Addresses - TODO: remove in a future release
	input = `{"addresses": {"rpc": "1.2.3.4"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.Addresses.RPC != "1.2.3.4" {
		t.Fatalf("bad: %#v", config)
	}

	// Domain socket permissions
	input = `{"unix_sockets": {"user": "500", "group": "500", "mode": "0700"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.UnixSockets.Usr != "500" {
		t.Fatalf("bad: %#v", config)
	}
	if config.UnixSockets.Grp != "500" {
		t.Fatalf("bad: %#v", config)
	}
	if config.UnixSockets.Perms != "0700" {
		t.Fatalf("bad: %#v", config)
	}

	// Disable updates
	input = `{"disable_update_check": true, "disable_anonymous_signature": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !config.DisableUpdateCheck {
		t.Fatalf("bad: %#v", config)
	}
	if !config.DisableAnonymousSignature {
		t.Fatalf("bad: %#v", config)
	}

	// HTTP API response header fields
	input = `{"http_api_response_headers": {"Access-Control-Allow-Origin": "*", "X-XSS-Protection": "1; mode=block"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.HTTPAPIResponseHeaders["Access-Control-Allow-Origin"] != "*" {
		t.Fatalf("bad: %#v", config)
	}

	if config.HTTPAPIResponseHeaders["X-XSS-Protection"] != "1; mode=block" {
		t.Fatalf("bad: %#v", config)
	}

	// Atlas configs
	input = `{
		"atlas_infrastructure": "hashicorp/prod",
		"atlas_token": "abcdefg",
		"atlas_acl_token": "123456789",
		"atlas_join": true,
		"atlas_endpoint": "foo.bar:1111"
}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.AtlasInfrastructure != "hashicorp/prod" {
		t.Fatalf("bad: %#v", config)
	}
	if config.AtlasToken != "abcdefg" {
		t.Fatalf("bad: %#v", config)
	}
	if config.AtlasACLToken != "123456789" {
		t.Fatalf("bad: %#v", config)
	}
	if !config.AtlasJoin {
		t.Fatalf("bad: %#v", config)
	}
	if config.AtlasEndpoint != "foo.bar:1111" {
		t.Fatalf("bad: %#v", config)
	}

	// Coordinate disable
	input = `{"disable_coordinates": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.DisableCoordinates != true {
		t.Fatalf("bad: coordinates not disabled: %#v", config)
	}

	// SessionTTLMin
	input = `{"session_ttl_min": "5s"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.SessionTTLMin != 5*time.Second {
		t.Fatalf("bad: %s %#v", config.SessionTTLMin.String(), config)
	}
}

func TestDecodeConfig_invalidKeys(t *testing.T) {
	input := `{"bad": "no way jose"}`
	_, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err == nil || !strings.Contains(err.Error(), "invalid keys") {
		t.Fatalf("should have rejected invalid config keys")
	}
}

func TestRetryJoinEC2(t *testing.T) {
	input := `{"retry_join_ec2": {
	  "region": "us-east-1",
		"tag_key": "ConsulRole",
		"tag_value": "Server",
		"access_key_id": "asdf",
		"secret_access_key": "qwerty"
	}}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RetryJoinEC2.Region != "us-east-1" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinEC2.TagKey != "ConsulRole" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinEC2.TagValue != "Server" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinEC2.AccessKeyID != "asdf" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinEC2.SecretAccessKey != "qwerty" {
		t.Fatalf("bad: %#v", config)
	}
}

func TestRetryJoinGCE(t *testing.T) {
	input := `{"retry_join_gce": {
	  "project_name": "test-project",
		"zone_pattern": "us-west1-a",
		"tag_value": "consul-server",
		"credentials_file": "/path/to/foo.json"
	}}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.RetryJoinGCE.ProjectName != "test-project" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinGCE.ZonePattern != "us-west1-a" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinGCE.TagValue != "consul-server" {
		t.Fatalf("bad: %#v", config)
	}
	if config.RetryJoinGCE.CredentialsFile != "/path/to/foo.json" {
		t.Fatalf("bad: %#v", config)
	}
}

func TestDecodeConfig_Performance(t *testing.T) {
	input := `{"performance": { "raft_multiplier": 3 }}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Performance.RaftMultiplier != 3 {
		t.Fatalf("bad: multiplier isn't set: %#v", config)
	}

	input = `{"performance": { "raft_multiplier": 11 }}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err == nil || !strings.Contains(err.Error(), "Performance.RaftMultiplier must be <=") {
		t.Fatalf("bad: %v", err)
	}
}

func TestDecodeConfig_Autopilot(t *testing.T) {
	input := `{"autopilot": {
	  "cleanup_dead_servers": true,
	  "last_contact_threshold": "100ms",
	  "max_trailing_logs": 10,
	  "server_stabilization_time": "10s",
	  "redundancy_zone_tag": "az",
	  "disable_upgrade_migration": true
	 }}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Autopilot.CleanupDeadServers == nil || !*config.Autopilot.CleanupDeadServers {
		t.Fatalf("bad: %#v", config)
	}
	if config.Autopilot.LastContactThreshold == nil || *config.Autopilot.LastContactThreshold != 100*time.Millisecond {
		t.Fatalf("bad: %#v", config)
	}
	if config.Autopilot.MaxTrailingLogs == nil || *config.Autopilot.MaxTrailingLogs != 10 {
		t.Fatalf("bad: %#v", config)
	}
	if config.Autopilot.ServerStabilizationTime == nil || *config.Autopilot.ServerStabilizationTime != 10*time.Second {
		t.Fatalf("bad: %#v", config)
	}
	if config.Autopilot.RedundancyZoneTag != "az" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Autopilot.DisableUpgradeMigration == nil || !*config.Autopilot.DisableUpgradeMigration {
		t.Fatalf("bad: %#v", config)
	}
}

func TestDecodeConfig_Services(t *testing.T) {
	input := `{
		"services": [
			{
				"id": "red0",
				"name": "redis",
				"tags": [
					"master"
				],
				"port": 6000,
				"check": {
					"script": "/bin/check_redis -p 6000",
					"interval": "5s",
					"ttl": "20s"
				},
				"checks": [
					{
						"script": "/bin/check_redis_read",
						"interval": "1m"
					},
					{
						"script": "/bin/check_redis_write",
						"interval": "1m"
					}
				]
			},
			{
				"id": "red1",
				"name": "redis",
				"tags": [
					"delayed",
					"slave"
				],
				"port": 7000,
				"check": {
					"script": "/bin/check_redis -p 7000",
					"interval": "30s",
					"ttl": "60s"
				}
			},
			{
				"id": "es0",
				"name": "elasticsearch",
				"port": 9200,
				"check": {
					"HTTP": "http://localhost:9200/_cluster_health",
					"interval": "10s",
					"timeout": "100ms"
				}
			}
		]
	}`

	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := &Config{
		Services: []*ServiceDefinition{
			&ServiceDefinition{
				Check: CheckType{
					Interval: 5 * time.Second,
					Script:   "/bin/check_redis -p 6000",
					TTL:      20 * time.Second,
				},
				Checks: CheckTypes{
					&CheckType{
						Interval: time.Minute,
						Script:   "/bin/check_redis_read",
					},
					&CheckType{
						Interval: time.Minute,
						Script:   "/bin/check_redis_write",
					},
				},
				ID:   "red0",
				Name: "redis",
				Tags: []string{
					"master",
				},
				Port: 6000,
			},
			&ServiceDefinition{
				Check: CheckType{
					Interval: 30 * time.Second,
					Script:   "/bin/check_redis -p 7000",
					TTL:      60 * time.Second,
				},
				ID:   "red1",
				Name: "redis",
				Tags: []string{
					"delayed",
					"slave",
				},
				Port: 7000,
			},
			&ServiceDefinition{
				Check: CheckType{
					HTTP:     "http://localhost:9200/_cluster_health",
					Interval: 10 * time.Second,
					Timeout:  100 * time.Millisecond,
				},
				ID:   "es0",
				Name: "elasticsearch",
				Port: 9200,
			},
		},
	}

	if !reflect.DeepEqual(config, expected) {
		t.Fatalf("bad: %#v", config)
	}
}

func TestDecodeConfig_verifyUniqueListeners(t *testing.T) {
	tests := []struct {
		name string
		cfg  string
		pass bool
	}{
		{
			"http_dns1",
			`{"addresses": {"http": "0.0.0.0", "dns": "127.0.0.1"}, "ports": {"dns": 8000}}`,
			true,
		},
		{
			"http_dns IP identical",
			`{"addresses": {"http": "0.0.0.0", "dns": "0.0.0.0"}, "ports": {"http": 8000, "dns": 8000}}`,
			false,
		},
	}

	for _, test := range tests {
		config, err := DecodeConfig(bytes.NewReader([]byte(test.cfg)))
		if err != nil {
			t.Fatalf("err: %s %s", test.name, err)
		}

		err = config.verifyUniqueListeners()
		if (err != nil && test.pass) || (err == nil && !test.pass) {
			t.Errorf("err: %s should have %v: %v: %v", test.name, test.pass, test.cfg, err)
		}
	}
}

func TestDecodeConfig_Checks(t *testing.T) {
	input := `{
		"checks": [
			{
				"id": "chk1",
				"name": "mem",
				"script": "/bin/check_mem",
				"interval": "5s"
			},
			{
				"id": "chk2",
				"name": "cpu",
				"script": "/bin/check_cpu",
				"interval": "10s"
			},
			{
				"id": "chk3",
				"name": "service:redis:tx",
				"script": "/bin/check_redis_tx",
				"interval": "1m",
				"service_id": "redis"
			},
			{
				"id": "chk4",
				"name": "service:elasticsearch:health",
				"HTTP": "http://localhost:9200/_cluster_health",
				"interval": "10s",
				"timeout": "100ms",
				"service_id": "elasticsearch"
			},
			{
				"id": "chk5",
				"name": "service:sslservice",
				"HTTP": "https://sslservice/status",
				"interval": "10s",
				"timeout": "100ms",
				"service_id": "sslservice"
			},
			{
				"id": "chk6",
				"name": "service:insecure-sslservice",
				"HTTP": "https://insecure-sslservice/status",
				"interval": "10s",
				"timeout": "100ms",
				"service_id": "insecure-sslservice",
				"tls_skip_verify": true
			}
		]
	}`

	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := &Config{
		Checks: []*CheckDefinition{
			&CheckDefinition{
				ID:   "chk1",
				Name: "mem",
				CheckType: CheckType{
					Script:   "/bin/check_mem",
					Interval: 5 * time.Second,
				},
			},
			&CheckDefinition{
				ID:   "chk2",
				Name: "cpu",
				CheckType: CheckType{
					Script:   "/bin/check_cpu",
					Interval: 10 * time.Second,
				},
			},
			&CheckDefinition{
				ID:        "chk3",
				Name:      "service:redis:tx",
				ServiceID: "redis",
				CheckType: CheckType{
					Script:   "/bin/check_redis_tx",
					Interval: time.Minute,
				},
			},
			&CheckDefinition{
				ID:        "chk4",
				Name:      "service:elasticsearch:health",
				ServiceID: "elasticsearch",
				CheckType: CheckType{
					HTTP:     "http://localhost:9200/_cluster_health",
					Interval: 10 * time.Second,
					Timeout:  100 * time.Millisecond,
				},
			},
			&CheckDefinition{
				ID:        "chk5",
				Name:      "service:sslservice",
				ServiceID: "sslservice",
				CheckType: CheckType{
					HTTP:          "https://sslservice/status",
					Interval:      10 * time.Second,
					Timeout:       100 * time.Millisecond,
					TLSSkipVerify: false,
				},
			},
			&CheckDefinition{
				ID:        "chk6",
				Name:      "service:insecure-sslservice",
				ServiceID: "insecure-sslservice",
				CheckType: CheckType{
					HTTP:          "https://insecure-sslservice/status",
					Interval:      10 * time.Second,
					Timeout:       100 * time.Millisecond,
					TLSSkipVerify: true,
				},
			},
		},
	}

	if !reflect.DeepEqual(config, expected) {
		t.Fatalf("bad: %#v", config)
	}
}

func TestDecodeConfig_Multiples(t *testing.T) {
	input := `{
		"services": [
			{
				"id": "red0",
				"name": "redis",
				"tags": [
					"master"
				],
				"port": 6000,
				"check": {
					"script": "/bin/check_redis -p 6000",
					"interval": "5s",
					"ttl": "20s"
				}
			}
		],
		"checks": [
			{
				"id": "chk1",
				"name": "mem",
				"script": "/bin/check_mem",
				"interval": "10s"
			}
		]
	}`

	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := &Config{
		Services: []*ServiceDefinition{
			&ServiceDefinition{
				Check: CheckType{
					Interval: 5 * time.Second,
					Script:   "/bin/check_redis -p 6000",
					TTL:      20 * time.Second,
				},
				ID:   "red0",
				Name: "redis",
				Tags: []string{
					"master",
				},
				Port: 6000,
			},
		},
		Checks: []*CheckDefinition{
			&CheckDefinition{
				ID:   "chk1",
				Name: "mem",
				CheckType: CheckType{
					Script:   "/bin/check_mem",
					Interval: 10 * time.Second,
				},
			},
		},
	}

	if !reflect.DeepEqual(config, expected) {
		t.Fatalf("bad: %#v", config)
	}
}

func TestDecodeConfig_Service(t *testing.T) {
	// Basics
	input := `{"service": {"id": "red1", "name": "redis", "tags": ["master"], "port":8000, "check": {"script": "/bin/check_redis", "interval": "10s", "ttl": "15s", "DeregisterCriticalServiceAfter": "90m" }}}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.Services) != 1 {
		t.Fatalf("missing service")
	}

	serv := config.Services[0]
	if serv.ID != "red1" {
		t.Fatalf("bad: %v", serv)
	}

	if serv.Name != "redis" {
		t.Fatalf("bad: %v", serv)
	}

	if !lib.StrContains(serv.Tags, "master") {
		t.Fatalf("bad: %v", serv)
	}

	if serv.Port != 8000 {
		t.Fatalf("bad: %v", serv)
	}

	if serv.Check.Script != "/bin/check_redis" {
		t.Fatalf("bad: %v", serv)
	}

	if serv.Check.Interval != 10*time.Second {
		t.Fatalf("bad: %v", serv)
	}

	if serv.Check.TTL != 15*time.Second {
		t.Fatalf("bad: %v", serv)
	}

	if serv.Check.DeregisterCriticalServiceAfter != 90*time.Minute {
		t.Fatalf("bad: %v", serv)
	}
}

func TestDecodeConfig_Check(t *testing.T) {
	// Basics
	input := `{"check": {"id": "chk1", "name": "mem", "notes": "foobar", "script": "/bin/check_redis", "interval": "10s", "ttl": "15s", "shell": "/bin/bash", "docker_container_id": "redis", "deregister_critical_service_after": "90s" }}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(config.Checks) != 1 {
		t.Fatalf("missing check")
	}

	chk := config.Checks[0]
	if chk.ID != "chk1" {
		t.Fatalf("bad: %v", chk)
	}

	if chk.Name != "mem" {
		t.Fatalf("bad: %v", chk)
	}

	if chk.Notes != "foobar" {
		t.Fatalf("bad: %v", chk)
	}

	if chk.Script != "/bin/check_redis" {
		t.Fatalf("bad: %v", chk)
	}

	if chk.Interval != 10*time.Second {
		t.Fatalf("bad: %v", chk)
	}

	if chk.TTL != 15*time.Second {
		t.Fatalf("bad: %v", chk)
	}

	if chk.Shell != "/bin/bash" {
		t.Fatalf("bad: %v", chk)
	}

	if chk.DockerContainerID != "redis" {
		t.Fatalf("bad: %v", chk)
	}

	if chk.DeregisterCriticalServiceAfter != 90*time.Second {
		t.Fatalf("bad: %v", chk)
	}
}

func TestMergeConfig(t *testing.T) {
	a := &Config{
		Bootstrap:              false,
		BootstrapExpect:        0,
		Datacenter:             "dc1",
		DataDir:                "/tmp/foo",
		Domain:                 "basic",
		LogLevel:               "debug",
		NodeID:                 "bar",
		NodeName:               "foo",
		ClientAddr:             "127.0.0.1",
		BindAddr:               "127.0.0.1",
		AdvertiseAddr:          "127.0.0.1",
		Server:                 false,
		LeaveOnTerm:            new(bool),
		SkipLeaveOnInt:         new(bool),
		EnableDebug:            false,
		CheckUpdateIntervalRaw: "8m",
		RetryIntervalRaw:       "10s",
		RetryIntervalWanRaw:    "10s",
		RetryJoinEC2: RetryJoinEC2{
			Region:          "us-east-1",
			TagKey:          "Key1",
			TagValue:        "Value1",
			AccessKeyID:     "nope",
			SecretAccessKey: "nope",
		},
		Telemetry: Telemetry{
			DisableHostname: false,
			StatsdAddr:      "nope",
			StatsiteAddr:    "nope",
			StatsitePrefix:  "nope",
			DogStatsdAddr:   "nope",
			DogStatsdTags:   []string{"nope"},
		},
		Meta: map[string]string{
			"key": "value1",
		},
	}

	b := &Config{
		Performance: Performance{
			RaftMultiplier: 99,
		},
		Bootstrap:       true,
		BootstrapExpect: 3,
		Datacenter:      "dc2",
		DataDir:         "/tmp/bar",
		DNSRecursors:    []string{"127.0.0.2:1001"},
		DNSConfig: DNSConfig{
			AllowStale:         Bool(false),
			EnableTruncate:     true,
			DisableCompression: true,
			MaxStale:           30 * time.Second,
			NodeTTL:            10 * time.Second,
			ServiceTTL: map[string]time.Duration{
				"api": 10 * time.Second,
			},
			UDPAnswerLimit:  4,
			RecursorTimeout: 30 * time.Second,
		},
		Domain:           "other",
		LogLevel:         "info",
		NodeID:           "bar",
		NodeName:         "baz",
		ClientAddr:       "127.0.0.2",
		BindAddr:         "127.0.0.2",
		AdvertiseAddr:    "127.0.0.2",
		AdvertiseAddrWan: "127.0.0.2",
		Ports: PortConfig{
			DNS:     1,
			HTTP:    2,
			SerfLan: 4,
			SerfWan: 5,
			Server:  6,
			HTTPS:   7,
		},
		Addresses: AddressConfig{
			DNS:   "127.0.0.1",
			HTTP:  "127.0.0.2",
			HTTPS: "127.0.0.4",
		},
		Server:         true,
		LeaveOnTerm:    Bool(true),
		SkipLeaveOnInt: Bool(true),
		RaftProtocol:   3,
		Autopilot: Autopilot{
			CleanupDeadServers:      Bool(true),
			LastContactThreshold:    Duration(time.Duration(10)),
			MaxTrailingLogs:         Uint64(10),
			ServerStabilizationTime: Duration(time.Duration(100)),
		},
		EnableDebug:            true,
		VerifyIncoming:         true,
		VerifyOutgoing:         true,
		CAFile:                 "test/ca.pem",
		CertFile:               "test/cert.pem",
		KeyFile:                "test/key.pem",
		TLSMinVersion:          "tls12",
		Checks:                 []*CheckDefinition{nil},
		Services:               []*ServiceDefinition{nil},
		StartJoin:              []string{"1.1.1.1"},
		StartJoinWan:           []string{"1.1.1.1"},
		EnableUi:               true,
		UiDir:                  "/opt/consul-ui",
		EnableSyslog:           true,
		RejoinAfterLeave:       true,
		RetryJoin:              []string{"1.1.1.1"},
		RetryIntervalRaw:       "10s",
		RetryInterval:          10 * time.Second,
		RetryJoinWan:           []string{"1.1.1.1"},
		RetryIntervalWanRaw:    "10s",
		RetryIntervalWan:       10 * time.Second,
		ReconnectTimeoutLanRaw: "24h",
		ReconnectTimeoutLan:    24 * time.Hour,
		ReconnectTimeoutWanRaw: "36h",
		ReconnectTimeoutWan:    36 * time.Hour,
		CheckUpdateInterval:    8 * time.Minute,
		CheckUpdateIntervalRaw: "8m",
		ACLToken:               "1111",
		ACLAgentMasterToken:    "2222",
		ACLAgentToken:          "3333",
		ACLMasterToken:         "4444",
		ACLDatacenter:          "dc2",
		ACLTTL:                 15 * time.Second,
		ACLTTLRaw:              "15s",
		ACLDownPolicy:          "deny",
		ACLDefaultPolicy:       "deny",
		ACLReplicationToken:    "8765309",
		ACLEnforceVersion8:     Bool(true),
		Watches: []map[string]interface{}{
			map[string]interface{}{
				"type":    "keyprefix",
				"prefix":  "foo/",
				"handler": "foobar",
			},
		},
		DisableRemoteExec: true,
		Telemetry: Telemetry{
			StatsiteAddr:    "127.0.0.1:7250",
			StatsitePrefix:  "stats_prefix",
			StatsdAddr:      "127.0.0.1:7251",
			DisableHostname: true,
			DogStatsdAddr:   "127.0.0.1:7254",
			DogStatsdTags:   []string{"tag_1:val_1", "tag_2:val_2"},
		},
		Meta: map[string]string{
			"key": "value2",
		},
		DisableUpdateCheck:        true,
		DisableAnonymousSignature: true,
		HTTPAPIResponseHeaders: map[string]string{
			"Access-Control-Allow-Origin": "*",
		},
		UnixSockets: UnixSocketConfig{
			UnixSocketPermissions{
				Usr:   "500",
				Grp:   "500",
				Perms: "0700",
			},
		},
		AtlasInfrastructure: "hashicorp/prod",
		AtlasToken:          "123456789",
		AtlasACLToken:       "abcdefgh",
		AtlasJoin:           true,
		RetryJoinEC2: RetryJoinEC2{
			Region:          "us-east-2",
			TagKey:          "Key2",
			TagValue:        "Value2",
			AccessKeyID:     "foo",
			SecretAccessKey: "bar",
		},
		SessionTTLMinRaw: "1000s",
		SessionTTLMin:    1000 * time.Second,
		AdvertiseAddrs: AdvertiseAddrsConfig{
			SerfLan:    &net.TCPAddr{},
			SerfLanRaw: "127.0.0.5:1231",
			SerfWan:    &net.TCPAddr{},
			SerfWanRaw: "127.0.0.5:1232",
			RPC:        &net.TCPAddr{},
			RPCRaw:     "127.0.0.5:1233",
		},
	}

	c := MergeConfig(a, b)

	if !reflect.DeepEqual(c, b) {
		t.Fatalf("should be equal %#v %#v", c, b)
	}
}

func TestReadConfigPaths_badPath(t *testing.T) {
	_, err := ReadConfigPaths([]string{"/i/shouldnt/exist/ever/rainbows"})
	if err == nil {
		t.Fatal("should have err")
	}
}

func TestReadConfigPaths_file(t *testing.T) {
	tf, err := ioutil.TempFile("", "consul")
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	tf.Write([]byte(`{"node_name":"bar"}`))
	tf.Close()
	defer os.Remove(tf.Name())

	config, err := ReadConfigPaths([]string{tf.Name()})
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.NodeName != "bar" {
		t.Fatalf("bad: %#v", config)
	}
}

func TestReadConfigPaths_dir(t *testing.T) {
	td, err := ioutil.TempDir("", "consul")
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	defer os.RemoveAll(td)

	err = ioutil.WriteFile(filepath.Join(td, "a.json"),
		[]byte(`{"node_name": "bar"}`), 0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	err = ioutil.WriteFile(filepath.Join(td, "b.json"),
		[]byte(`{"node_name": "baz"}`), 0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// A non-json file, shouldn't be read
	err = ioutil.WriteFile(filepath.Join(td, "c"),
		[]byte(`{"node_name": "bad"}`), 0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// An empty file shouldn't be read
	err = ioutil.WriteFile(filepath.Join(td, "d.json"),
		[]byte{}, 0664)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	config, err := ReadConfigPaths([]string{td})
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if config.NodeName != "baz" {
		t.Fatalf("bad: %#v", config)
	}
}

func TestUnixSockets(t *testing.T) {
	path1, ok := unixSocketAddr("unix:///path/to/socket")
	if !ok || path1 != "/path/to/socket" {
		t.Fatalf("bad: %v %v", ok, path1)
	}

	path2, ok := unixSocketAddr("notunix://blah")
	if ok || path2 != "" {
		t.Fatalf("bad: %v %v", ok, path2)
	}
}