1package checks
2
3import (
4	"crypto/tls"
5	"flag"
6	"fmt"
7	"io/ioutil"
8	"log"
9	"net"
10	"os"
11	"testing"
12	"time"
13
14	"github.com/hashicorp/consul/agent/mock"
15	"github.com/hashicorp/consul/agent/structs"
16	"github.com/hashicorp/consul/api"
17	"github.com/hashicorp/consul/sdk/testutil/retry"
18	"github.com/hashicorp/go-hclog"
19
20	"google.golang.org/grpc"
21	"google.golang.org/grpc/health"
22	hv1 "google.golang.org/grpc/health/grpc_health_v1"
23)
24
25var (
26	port         int
27	server       string
28	svcHealthy   string
29	svcUnhealthy string
30	svcMissing   string
31)
32
33func startServer() (*health.Server, *grpc.Server) {
34	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
35	if err != nil {
36		log.Fatalf("failed to listen: %v", err)
37	}
38	grpcServer := grpc.NewServer()
39	server := health.NewServer()
40	hv1.RegisterHealthServer(grpcServer, server)
41	go grpcServer.Serve(listener)
42	return server, grpcServer
43}
44
45func init() {
46	flag.IntVar(&port, "grpc-stub-port", 54321, "port for the gRPC stub server")
47}
48
49func TestMain(m *testing.M) {
50	flag.Parse()
51
52	healthy := "healthy"
53	unhealthy := "unhealthy"
54	missing := "missing"
55
56	srv, grpcStubApp := startServer()
57	srv.SetServingStatus(healthy, hv1.HealthCheckResponse_SERVING)
58	srv.SetServingStatus(unhealthy, hv1.HealthCheckResponse_NOT_SERVING)
59
60	server = fmt.Sprintf("%s:%d", "localhost", port)
61	svcHealthy = fmt.Sprintf("%s/%s", server, healthy)
62	svcUnhealthy = fmt.Sprintf("%s/%s", server, unhealthy)
63	svcMissing = fmt.Sprintf("%s/%s", server, missing)
64
65	result := 1
66	defer func() {
67		grpcStubApp.Stop()
68		os.Exit(result)
69	}()
70
71	result = m.Run()
72}
73
74func TestCheck(t *testing.T) {
75	type args struct {
76		target    string
77		timeout   time.Duration
78		tlsConfig *tls.Config
79	}
80	tests := []struct {
81		name    string
82		args    args
83		healthy bool
84	}{
85		// successes
86		{"should pass for healthy server", args{server, time.Second, nil}, true},
87		{"should pass for healthy service", args{svcHealthy, time.Second, nil}, true},
88
89		// failures
90		{"should fail for unhealthy service", args{svcUnhealthy, time.Second, nil}, false},
91		{"should fail for missing service", args{svcMissing, time.Second, nil}, false},
92		{"should timeout for healthy service", args{server, time.Nanosecond, nil}, false},
93		{"should fail if probe is secure, but server is not", args{server, time.Second, &tls.Config{}}, false},
94	}
95	for _, tt := range tests {
96		t.Run(tt.name, func(t *testing.T) {
97			probe := NewGrpcHealthProbe(tt.args.target, tt.args.timeout, tt.args.tlsConfig)
98			actualError := probe.Check(tt.args.target)
99			actuallyHealthy := actualError == nil
100			if tt.healthy != actuallyHealthy {
101				t.Errorf("FAIL: %s. Expected healthy %t, but err == %v", tt.name, tt.healthy, actualError)
102			}
103		})
104	}
105}
106
107func TestGRPC_Proxied(t *testing.T) {
108	t.Parallel()
109
110	notif := mock.NewNotify()
111	logger := hclog.New(&hclog.LoggerOptions{
112		Name:   uniqueID(),
113		Output: ioutil.Discard,
114	})
115
116	statusHandler := NewStatusHandler(notif, logger, 0, 0)
117	cid := structs.NewCheckID("foo", nil)
118
119	check := &CheckGRPC{
120		CheckID:       cid,
121		GRPC:          "",
122		Interval:      10 * time.Millisecond,
123		Logger:        logger,
124		ProxyGRPC:     server,
125		StatusHandler: statusHandler,
126	}
127	check.Start()
128	defer check.Stop()
129
130	// If ProxyGRPC is set, check() reqs should go to that address
131	retry.Run(t, func(r *retry.R) {
132		if got, want := notif.Updates(cid), 2; got < want {
133			r.Fatalf("got %d updates want at least %d", got, want)
134		}
135		if got, want := notif.State(cid), api.HealthPassing; got != want {
136			r.Fatalf("got state %q want %q", got, want)
137		}
138	})
139}
140
141func TestGRPC_NotProxied(t *testing.T) {
142	t.Parallel()
143
144	notif := mock.NewNotify()
145	logger := hclog.New(&hclog.LoggerOptions{
146		Name:   uniqueID(),
147		Output: ioutil.Discard,
148	})
149
150	statusHandler := NewStatusHandler(notif, logger, 0, 0)
151	cid := structs.NewCheckID("foo", nil)
152
153	check := &CheckGRPC{
154		CheckID:       cid,
155		GRPC:          server,
156		Interval:      10 * time.Millisecond,
157		Logger:        logger,
158		ProxyGRPC:     "",
159		StatusHandler: statusHandler,
160	}
161	check.Start()
162	defer check.Stop()
163
164	// If ProxyGRPC is not set, check() reqs should go to check.GRPC
165	retry.Run(t, func(r *retry.R) {
166		if got, want := notif.Updates(cid), 2; got < want {
167			r.Fatalf("got %d updates want at least %d", got, want)
168		}
169		if got, want := notif.State(cid), api.HealthPassing; got != want {
170			r.Fatalf("got state %q want %q", got, want)
171		}
172	})
173}
174