// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package discovery 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/cache" cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/sdk/testutil" ) // Test_FetchVirtualIP tests the FetchVirtualIP method in scenarios where the RPC // call succeeds and fails. func Test_FetchVirtualIP(t *testing.T) { // set these to confirm that RPC call does not use them for this particular RPC rc := &config.RuntimeConfig{ DNSAllowStale: true, DNSMaxStale: 100, DNSUseCache: true, DNSCacheMaxAge: 100, } tests := []struct { name string queryPayload *QueryPayload context Context expectedResult *Result expectedErr error }{ { name: "FetchVirtualIP returns result", queryPayload: &QueryPayload{ Name: "db", Tenancy: QueryTenancy{ Peer: "test-peer", Namespace: defaultTestNamespace, Partition: defaultTestPartition, }, }, context: Context{ Token: "test-token", }, expectedResult: &Result{ Service: &Location{ Name: "db", Address: "192.168.10.10", }, Type: ResultTypeVirtual, }, expectedErr: nil, }, { name: "FetchVirtualIP returns error", queryPayload: &QueryPayload{ Name: "db", Tenancy: QueryTenancy{ Peer: "test-peer", Namespace: defaultTestNamespace, Partition: defaultTestPartition}, }, context: Context{ Token: "test-token", }, expectedResult: nil, expectedErr: errors.New("test-error"), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { logger := testutil.Logger(t) mockRPC := cachetype.NewMockRPC(t) mockRPC.On("RPC", mock.Anything, "Catalog.VirtualIPForService", mock.Anything, mock.Anything). Return(tc.expectedErr). Run(func(args mock.Arguments) { req := args.Get(2).(*structs.ServiceSpecificRequest) // validate RPC options are not set from config for the VirtuaLIPForService RPC require.False(t, req.AllowStale) require.Equal(t, time.Duration(0), req.MaxStaleDuration) require.False(t, req.UseCache) require.Equal(t, time.Duration(0), req.MaxAge) // validate RPC options are set correctly from the queryPayload and context require.Equal(t, tc.queryPayload.Tenancy.Peer, req.PeerName) require.Equal(t, tc.queryPayload.Tenancy.Namespace, req.EnterpriseMeta.NamespaceOrEmpty()) require.Equal(t, tc.queryPayload.Tenancy.Partition, req.EnterpriseMeta.PartitionOrEmpty()) require.Equal(t, tc.context.Token, req.QueryOptions.Token) if tc.expectedErr == nil { // set the out parameter to ensure that it is used to formulate the result.Address reply := args.Get(3).(*string) *reply = tc.expectedResult.Service.Address } }) translateServicePortFunc := func(dc string, port int, taggedAddresses map[string]structs.ServiceAddress) int { return 0 } rpcFuncForServiceNodes := func(ctx context.Context, req structs.ServiceSpecificRequest) (structs.IndexedCheckServiceNodes, cache.ResultMeta, error) { return structs.IndexedCheckServiceNodes{}, cache.ResultMeta{}, nil } rpcFuncForSamenessGroup := func(ctx context.Context, req *structs.ConfigEntryQuery) (structs.SamenessGroupConfigEntry, cache.ResultMeta, error) { return structs.SamenessGroupConfigEntry{}, cache.ResultMeta{}, nil } getFromCacheFunc := func(ctx context.Context, t string, r cache.Request) (interface{}, cache.ResultMeta, error) { return nil, cache.ResultMeta{}, nil } df := NewV1DataFetcher(rc, acl.DefaultEnterpriseMeta(), getFromCacheFunc, mockRPC.RPC, rpcFuncForServiceNodes, rpcFuncForSamenessGroup, translateServicePortFunc, logger) result, err := df.FetchVirtualIP(tc.context, tc.queryPayload) require.Equal(t, tc.expectedErr, err) require.Equal(t, tc.expectedResult, result) }) } } // Test_FetchEndpoints tests the FetchEndpoints method in scenarios where the RPC // call succeeds and fails. func Test_FetchEndpoints(t *testing.T) { // set these to confirm that RPC call does not use them for this particular RPC rc := &config.RuntimeConfig{ DNSAllowStale: true, DNSMaxStale: 100, DNSUseCache: true, DNSCacheMaxAge: 100, } ctx := Context{ Token: "test-token", } expectedResults := []*Result{ { Node: &Location{ Name: "node-name", Address: "node-address", TaggedAddresses: map[string]*TaggedAddress{}, }, Service: &Location{ Name: "service-name", Address: "service-address", TaggedAddresses: map[string]*TaggedAddress{}, }, Type: ResultTypeService, DNS: DNSConfig{ Weight: 1, }, Ports: []Port{ { Number: 0, }, }, Tenancy: ResultTenancy{ PeerName: "test-peer", }, }, } logger := testutil.Logger(t) mockRPC := cachetype.NewMockRPC(t) translateServicePortFunc := func(dc string, port int, taggedAddresses map[string]structs.ServiceAddress) int { return 0 } rpcFuncForSamenessGroup := func(ctx context.Context, req *structs.ConfigEntryQuery) (structs.SamenessGroupConfigEntry, cache.ResultMeta, error) { return structs.SamenessGroupConfigEntry{}, cache.ResultMeta{}, nil } getFromCacheFunc := func(ctx context.Context, t string, r cache.Request) (interface{}, cache.ResultMeta, error) { return nil, cache.ResultMeta{}, nil } rpcFuncForServiceNodes := func(ctx context.Context, req structs.ServiceSpecificRequest) (structs.IndexedCheckServiceNodes, cache.ResultMeta, error) { return structs.IndexedCheckServiceNodes{ Nodes: []structs.CheckServiceNode{ { Node: &structs.Node{ Address: "node-address", Node: "node-name", }, Service: &structs.NodeService{ Address: "service-address", Service: "service-name", }, }, }, }, cache.ResultMeta{}, nil } queryPayload := &QueryPayload{ Name: "service-name", Tenancy: QueryTenancy{ Peer: "test-peer", Namespace: defaultTestNamespace, Partition: defaultTestPartition, }, } df := NewV1DataFetcher(rc, acl.DefaultEnterpriseMeta(), getFromCacheFunc, mockRPC.RPC, rpcFuncForServiceNodes, rpcFuncForSamenessGroup, translateServicePortFunc, logger) results, err := df.FetchEndpoints(ctx, queryPayload, LookupTypeService) require.NoError(t, err) require.Equal(t, expectedResults, results) }