// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package proxycfgglue import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/submatview" "github.com/hashicorp/consul/proto/private/pbsubscribe" "github.com/hashicorp/consul/sdk/testutil" ) func TestServerHealth(t *testing.T) { t.Run("remote queries are delegated to the remote source", func(t *testing.T) { var ( ctx = context.Background() req = &structs.ServiceSpecificRequest{Datacenter: "dc2"} correlationID = "correlation-id" ch = make(chan<- proxycfg.UpdateEvent) result = errors.New("KABOOM") ) remoteSource := newMockHealth(t) remoteSource.On("Notify", ctx, req, correlationID, ch).Return(result) dataSource := ServerHealth(ServerDataSourceDeps{Datacenter: "dc1"}, remoteSource) err := dataSource.Notify(ctx, req, correlationID, ch) require.Equal(t, result, err) }) t.Run("local queries are served from a materialized view", func(t *testing.T) { // Note: the view is tested more thoroughly in the agent/rpcclient/health // package, so this is more of a high-level integration test with the local // materializer. const ( index uint64 = 123 datacenter = "dc1" serviceName = "web" ) logger := testutil.Logger(t) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) store := submatview.NewStore(logger) go store.Run(ctx) publisher := stream.NewEventPublisher(10 * time.Second) publisher.RegisterHandler(pbsubscribe.Topic_ServiceHealth, func(stream.SubscribeRequest, stream.SnapshotAppender) (uint64, error) { return index, nil }, true) go publisher.Run(ctx) dataSource := ServerHealth(ServerDataSourceDeps{ Datacenter: datacenter, ACLResolver: newStaticResolver(acl.ManageAll()), ViewStore: store, EventPublisher: publisher, Logger: logger, }, nil) eventCh := make(chan proxycfg.UpdateEvent) require.NoError(t, dataSource.Notify(ctx, &structs.ServiceSpecificRequest{ Datacenter: datacenter, ServiceName: serviceName, }, "", eventCh)) testutil.RunStep(t, "initial state", func(t *testing.T) { result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) require.Empty(t, result.Nodes) }) testutil.RunStep(t, "register services", func(t *testing.T) { publisher.Publish([]stream.Event{ { Index: index + 1, Topic: pbsubscribe.Topic_ServiceHealth, Payload: &state.EventPayloadCheckServiceNode{ Op: pbsubscribe.CatalogOp_Register, Value: &structs.CheckServiceNode{ Node: &structs.Node{Node: "node1"}, Service: &structs.NodeService{Service: serviceName}, }, }, }, { Index: index + 1, Topic: pbsubscribe.Topic_ServiceHealth, Payload: &state.EventPayloadCheckServiceNode{ Op: pbsubscribe.CatalogOp_Register, Value: &structs.CheckServiceNode{ Node: &structs.Node{Node: "node2"}, Service: &structs.NodeService{Service: serviceName}, }, }, }, }) result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) require.Len(t, result.Nodes, 2) }) testutil.RunStep(t, "deregister service", func(t *testing.T) { publisher.Publish([]stream.Event{ { Index: index + 2, Topic: pbsubscribe.Topic_ServiceHealth, Payload: &state.EventPayloadCheckServiceNode{ Op: pbsubscribe.CatalogOp_Deregister, Value: &structs.CheckServiceNode{ Node: &structs.Node{Node: "node2"}, Service: &structs.NodeService{Service: serviceName}, }, }, }, }) result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) require.Len(t, result.Nodes, 1) }) }) } func newMockHealth(t *testing.T) *mockHealth { mock := &mockHealth{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } type mockHealth struct { mock.Mock } func (m *mockHealth) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { return m.Called(ctx, req, correlationID, ch).Error(0) }