google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/channel_test.go (about)

     1  /*
     2   *
     3   * Copyright 2025 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xdsclient
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"net"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/google/go-cmp/cmp/cmpopts"
    31  	"github.com/google/uuid"
    32  	"google.golang.org/grpc/credentials/insecure"
    33  	"google.golang.org/grpc/xds/internal/clients"
    34  	"google.golang.org/grpc/xds/internal/clients/grpctransport"
    35  	"google.golang.org/grpc/xds/internal/clients/internal/testutils"
    36  	"google.golang.org/grpc/xds/internal/clients/internal/testutils/e2e"
    37  	"google.golang.org/grpc/xds/internal/clients/internal/testutils/fakeserver"
    38  	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
    39  	"google.golang.org/protobuf/testing/protocmp"
    40  	"google.golang.org/protobuf/types/known/anypb"
    41  
    42  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    43  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    44  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    45  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    46  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    47  )
    48  
    49  // xdsChannelForTest creates an xdsChannel to the specified serverURI for
    50  // testing purposes.
    51  func xdsChannelForTest(t *testing.T, serverURI, nodeID string, watchExpiryTimeout time.Duration) *xdsChannel {
    52  	t.Helper()
    53  
    54  	// Create a grpc transport to the above management server.
    55  	si := clients.ServerIdentifier{
    56  		ServerURI:  serverURI,
    57  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
    58  	}
    59  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
    60  	tr, err := (grpctransport.NewBuilder(configs)).Build(si)
    61  	if err != nil {
    62  		t.Fatalf("Failed to create a transport for server config %v: %v", si, err)
    63  	}
    64  
    65  	serverCfg := ServerConfig{
    66  		ServerIdentifier: si,
    67  	}
    68  	clientConfig := Config{
    69  		Servers:       []ServerConfig{serverCfg},
    70  		Node:          clients.Node{ID: nodeID},
    71  		ResourceTypes: map[string]ResourceType{xdsresource.V3ListenerURL: listenerType},
    72  	}
    73  	// Create an xdsChannel that uses everything set up above.
    74  	xc, err := newXDSChannel(xdsChannelOpts{
    75  		transport:          tr,
    76  		serverConfig:       &serverCfg,
    77  		clientConfig:       &clientConfig,
    78  		eventHandler:       newTestEventHandler(),
    79  		watchExpiryTimeout: watchExpiryTimeout,
    80  	})
    81  	if err != nil {
    82  		t.Fatalf("Failed to create xdsChannel: %v", err)
    83  	}
    84  	t.Cleanup(func() { xc.close() })
    85  	return xc
    86  }
    87  
    88  // verifyUpdateAndMetadata verifies that the event handler received the expected
    89  // updates and metadata.  It checks that the received resource type matches the
    90  // expected type, and that the received updates and metadata match the expected
    91  // values. The function ignores the timestamp fields in the metadata, as those
    92  // are expected to be different.
    93  func verifyUpdateAndMetadata(ctx context.Context, t *testing.T, eh *testEventHandler, wantUpdates map[string]dataAndErrTuple, wantMD xdsresource.UpdateMetadata) {
    94  	t.Helper()
    95  
    96  	gotTyp, gotUpdates, gotMD, err := eh.waitForUpdate(ctx)
    97  	if err != nil {
    98  		t.Fatalf("Timeout when waiting for update callback to be invoked on the event handler")
    99  	}
   100  
   101  	if gotTyp != listenerType {
   102  		t.Fatalf("Got resource type %v, want %v", gotTyp, listenerType)
   103  	}
   104  	opts := cmp.Options{
   105  		protocmp.Transform(),
   106  		cmpopts.EquateEmpty(),
   107  		cmpopts.EquateErrors(),
   108  		cmpopts.IgnoreFields(xdsresource.UpdateMetadata{}, "Timestamp"),
   109  		cmpopts.IgnoreFields(xdsresource.UpdateErrorMetadata{}, "Timestamp"),
   110  	}
   111  	if diff := cmp.Diff(wantUpdates, gotUpdates, opts); diff != "" {
   112  		t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %+v\n got: %+v", diff, wantUpdates, gotUpdates)
   113  	}
   114  	if diff := cmp.Diff(wantMD, gotMD, opts); diff != "" {
   115  		t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %v\n got: %v", diff, wantMD, gotMD)
   116  	}
   117  }
   118  
   119  // Tests different failure cases when creating a new xdsChannel. It checks that
   120  // the xdsChannel creation fails when any of the required options (transport,
   121  // serverConfig, bootstrapConfig, or resourceTypeGetter) are missing or nil.
   122  func (s) TestChannel_New_FailureCases(t *testing.T) {
   123  	type fakeTransport struct {
   124  		clients.Transport
   125  	}
   126  
   127  	tests := []struct {
   128  		name       string
   129  		opts       xdsChannelOpts
   130  		wantErrStr string
   131  	}{
   132  		{
   133  			name:       "emptyTransport",
   134  			opts:       xdsChannelOpts{},
   135  			wantErrStr: "transport is nil",
   136  		},
   137  		{
   138  			name:       "emptyServerConfig",
   139  			opts:       xdsChannelOpts{transport: &fakeTransport{}},
   140  			wantErrStr: "serverConfig is nil",
   141  		},
   142  		{
   143  			name: "emptyCConfig",
   144  			opts: xdsChannelOpts{
   145  				transport:    &fakeTransport{},
   146  				serverConfig: &ServerConfig{},
   147  			},
   148  			wantErrStr: "clientConfig is nil",
   149  		},
   150  		{
   151  			name: "emptyEventHandler",
   152  			opts: xdsChannelOpts{
   153  				transport:    &fakeTransport{},
   154  				serverConfig: &ServerConfig{},
   155  				clientConfig: &Config{},
   156  			},
   157  			wantErrStr: "eventHandler is nil",
   158  		},
   159  	}
   160  
   161  	for _, test := range tests {
   162  		t.Run(test.name, func(t *testing.T) {
   163  			if _, err := newXDSChannel(test.opts); err == nil || !strings.Contains(err.Error(), test.wantErrStr) {
   164  				t.Fatalf("newXDSChannel() = %v, want %q", err, test.wantErrStr)
   165  			}
   166  		})
   167  	}
   168  }
   169  
   170  // Tests different scenarios of the xdsChannel receiving a response from the
   171  // management server. In all scenarios, the xdsChannel is expected to pass the
   172  // received responses as-is to the resource parsing functionality specified by
   173  // the resourceTypeGetter.
   174  func (s) TestChannel_ADS_HandleResponseFromManagementServer(t *testing.T) {
   175  	const (
   176  		listenerName1 = "listener-name-1"
   177  		listenerName2 = "listener-name-2"
   178  		routeName     = "route-name"
   179  		clusterName   = "cluster-name"
   180  	)
   181  	var (
   182  		badlyMarshaledResource = &anypb.Any{
   183  			TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener",
   184  			Value:   []byte{1, 2, 3, 4},
   185  		}
   186  		apiListener = &v3listenerpb.ApiListener{
   187  			ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   188  				RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   189  					RouteConfig: &v3routepb.RouteConfiguration{
   190  						Name: routeName},
   191  				},
   192  			}),
   193  		}
   194  		listener1 = testutils.MarshalAny(t, &v3listenerpb.Listener{
   195  			Name:        listenerName1,
   196  			ApiListener: apiListener,
   197  		})
   198  		listener2 = testutils.MarshalAny(t, &v3listenerpb.Listener{
   199  			Name:        listenerName2,
   200  			ApiListener: apiListener,
   201  		})
   202  	)
   203  
   204  	tests := []struct {
   205  		desc                     string
   206  		resourceNamesToRequest   []string
   207  		managementServerResponse *v3discoverypb.DiscoveryResponse
   208  		wantUpdates              map[string]dataAndErrTuple
   209  		wantMD                   xdsresource.UpdateMetadata
   210  		wantErr                  error
   211  	}{
   212  		{
   213  			desc:                   "one bad resource - deserialization failure",
   214  			resourceNamesToRequest: []string{listenerName1},
   215  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   216  				VersionInfo: "0",
   217  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   218  				Resources:   []*anypb.Any{badlyMarshaledResource},
   219  			},
   220  			wantUpdates: nil, // No updates expected as the response runs into unmarshaling errors.
   221  			wantMD: xdsresource.UpdateMetadata{
   222  				Status:  xdsresource.ServiceStatusNACKed,
   223  				Version: "0",
   224  				ErrState: &xdsresource.UpdateErrorMetadata{
   225  					Version: "0",
   226  					Err:     cmpopts.AnyError,
   227  				},
   228  			},
   229  			wantErr: cmpopts.AnyError,
   230  		},
   231  		{
   232  			desc:                   "one bad resource - validation failure",
   233  			resourceNamesToRequest: []string{listenerName1},
   234  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   235  				VersionInfo: "0",
   236  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   237  				Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{
   238  					Name: listenerName1,
   239  					ApiListener: &v3listenerpb.ApiListener{
   240  						ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   241  							RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
   242  						}),
   243  					},
   244  				})},
   245  			},
   246  			wantUpdates: map[string]dataAndErrTuple{
   247  				listenerName1: {
   248  					Err: cmpopts.AnyError,
   249  				},
   250  			},
   251  			wantMD: xdsresource.UpdateMetadata{
   252  				Status:  xdsresource.ServiceStatusNACKed,
   253  				Version: "0",
   254  				ErrState: &xdsresource.UpdateErrorMetadata{
   255  					Version: "0",
   256  					Err:     cmpopts.AnyError,
   257  				},
   258  			},
   259  		},
   260  		{
   261  			desc:                   "two bad resources",
   262  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   263  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   264  				VersionInfo: "0",
   265  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   266  				Resources: []*anypb.Any{
   267  					badlyMarshaledResource,
   268  					testutils.MarshalAny(t, &v3listenerpb.Listener{
   269  						Name: listenerName2,
   270  						ApiListener: &v3listenerpb.ApiListener{
   271  							ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   272  								RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
   273  							}),
   274  						},
   275  					}),
   276  				},
   277  			},
   278  			wantUpdates: map[string]dataAndErrTuple{
   279  				listenerName2: {
   280  					Err: cmpopts.AnyError,
   281  				},
   282  			},
   283  			wantMD: xdsresource.UpdateMetadata{
   284  				Status:  xdsresource.ServiceStatusNACKed,
   285  				Version: "0",
   286  				ErrState: &xdsresource.UpdateErrorMetadata{
   287  					Version: "0",
   288  					Err:     cmpopts.AnyError,
   289  				},
   290  			},
   291  		},
   292  		{
   293  			desc:                   "one good resource",
   294  			resourceNamesToRequest: []string{listenerName1},
   295  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   296  				VersionInfo: "0",
   297  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   298  				Resources:   []*anypb.Any{listener1},
   299  			},
   300  			wantUpdates: map[string]dataAndErrTuple{
   301  				listenerName1: {
   302  					Resource: &listenerResourceData{Resource: listenerUpdate{
   303  						RouteConfigName: routeName,
   304  						Raw:             listener1.GetValue(),
   305  					}},
   306  				},
   307  			},
   308  			wantMD: xdsresource.UpdateMetadata{
   309  				Status:  xdsresource.ServiceStatusACKed,
   310  				Version: "0",
   311  			},
   312  		},
   313  		{
   314  			desc:                   "one good and one bad - deserialization failure",
   315  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   316  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   317  				VersionInfo: "0",
   318  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   319  				Resources: []*anypb.Any{
   320  					badlyMarshaledResource,
   321  					listener2,
   322  				},
   323  			},
   324  			wantUpdates: map[string]dataAndErrTuple{
   325  				listenerName2: {
   326  					Resource: &listenerResourceData{Resource: listenerUpdate{
   327  						RouteConfigName: routeName,
   328  						Raw:             listener2.GetValue(),
   329  					}},
   330  				},
   331  			},
   332  			wantMD: xdsresource.UpdateMetadata{
   333  				Status:  xdsresource.ServiceStatusNACKed,
   334  				Version: "0",
   335  				ErrState: &xdsresource.UpdateErrorMetadata{
   336  					Version: "0",
   337  					Err:     cmpopts.AnyError,
   338  				},
   339  			},
   340  		},
   341  		{
   342  			desc:                   "one good and one bad - validation failure",
   343  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   344  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   345  				VersionInfo: "0",
   346  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   347  				Resources: []*anypb.Any{
   348  					testutils.MarshalAny(t, &v3listenerpb.Listener{
   349  						Name: listenerName1,
   350  						ApiListener: &v3listenerpb.ApiListener{
   351  							ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   352  								RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
   353  							}),
   354  						},
   355  					}),
   356  					listener2,
   357  				},
   358  			},
   359  			wantUpdates: map[string]dataAndErrTuple{
   360  				listenerName1: {Err: cmpopts.AnyError},
   361  				listenerName2: {
   362  					Resource: &listenerResourceData{Resource: listenerUpdate{
   363  						RouteConfigName: routeName,
   364  						Raw:             listener2.GetValue(),
   365  					}},
   366  				},
   367  			},
   368  			wantMD: xdsresource.UpdateMetadata{
   369  				Status:  xdsresource.ServiceStatusNACKed,
   370  				Version: "0",
   371  				ErrState: &xdsresource.UpdateErrorMetadata{
   372  					Version: "0",
   373  					Err:     cmpopts.AnyError,
   374  				},
   375  			},
   376  		},
   377  		{
   378  			desc:                   "two good resources",
   379  			resourceNamesToRequest: []string{listenerName1, listenerName2},
   380  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   381  				VersionInfo: "0",
   382  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   383  				Resources:   []*anypb.Any{listener1, listener2},
   384  			},
   385  			wantUpdates: map[string]dataAndErrTuple{
   386  				listenerName1: {
   387  					Resource: &listenerResourceData{Resource: listenerUpdate{
   388  						RouteConfigName: routeName,
   389  						Raw:             listener1.GetValue(),
   390  					}},
   391  				},
   392  				listenerName2: {
   393  					Resource: &listenerResourceData{Resource: listenerUpdate{
   394  						RouteConfigName: routeName,
   395  						Raw:             listener2.GetValue(),
   396  					}},
   397  				},
   398  			},
   399  			wantMD: xdsresource.UpdateMetadata{
   400  				Status:  xdsresource.ServiceStatusACKed,
   401  				Version: "0",
   402  			},
   403  		},
   404  		{
   405  			desc:                   "two resources when we requested one",
   406  			resourceNamesToRequest: []string{listenerName1},
   407  			managementServerResponse: &v3discoverypb.DiscoveryResponse{
   408  				VersionInfo: "0",
   409  				TypeUrl:     "type.googleapis.com/envoy.config.listener.v3.Listener",
   410  				Resources:   []*anypb.Any{listener1, listener2},
   411  			},
   412  			wantUpdates: map[string]dataAndErrTuple{
   413  				listenerName1: {
   414  					Resource: &listenerResourceData{Resource: listenerUpdate{
   415  						RouteConfigName: routeName,
   416  						Raw:             listener1.GetValue(),
   417  					}},
   418  				},
   419  				listenerName2: {
   420  					Resource: &listenerResourceData{Resource: listenerUpdate{
   421  						RouteConfigName: routeName,
   422  						Raw:             listener2.GetValue(),
   423  					}},
   424  				},
   425  			},
   426  			wantMD: xdsresource.UpdateMetadata{
   427  				Status:  xdsresource.ServiceStatusACKed,
   428  				Version: "0",
   429  			},
   430  		},
   431  	}
   432  
   433  	for _, test := range tests {
   434  		t.Run(test.desc, func(t *testing.T) {
   435  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   436  			defer cancel()
   437  
   438  			// Start a fake xDS management server and configure the response it
   439  			// would send to its client.
   440  			mgmtServer, cleanup, err := fakeserver.StartServer(nil)
   441  			if err != nil {
   442  				t.Fatalf("Failed to start fake xDS server: %v", err)
   443  			}
   444  			defer cleanup()
   445  			t.Logf("Started xDS management server on %s", mgmtServer.Address)
   446  			mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse}
   447  
   448  			// Create an xdsChannel for the test with a long watch expiry timer
   449  			// to ensure that watches don't expire for the duration of the test.
   450  			nodeID := uuid.New().String()
   451  			xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout)
   452  			defer xc.close()
   453  
   454  			// Subscribe to the resources specified in the test table.
   455  			for _, name := range test.resourceNamesToRequest {
   456  				xc.subscribe(listenerType, name)
   457  			}
   458  
   459  			// Wait for an update callback on the event handler and verify the
   460  			// contents of the update and the metadata.
   461  			verifyUpdateAndMetadata(ctx, t, xc.eventHandler.(*testEventHandler), test.wantUpdates, test.wantMD)
   462  		})
   463  	}
   464  }
   465  
   466  // Tests that the xdsChannel correctly handles the expiry of a watch for a
   467  // resource by ensuring that the watch expiry callback is invoked on the event
   468  // handler with the expected resource type and name.
   469  func (s) TestChannel_ADS_HandleResponseWatchExpiry(t *testing.T) {
   470  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   471  	defer cancel()
   472  
   473  	// Start an xDS management server, but do not configure any resources on it.
   474  	// This will result in the watch for a resource to timeout.
   475  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   476  
   477  	// Create an xdsChannel for the test with a short watch expiry timer to
   478  	// ensure that the test does not run very long, as it needs to wait for the
   479  	// watch to expire.
   480  	nodeID := uuid.New().String()
   481  	xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestShortTimeout)
   482  	defer xc.close()
   483  
   484  	// Subscribe to a listener resource.
   485  	const listenerName = "listener-name"
   486  	xc.subscribe(listenerType, listenerName)
   487  
   488  	// Wait for the watch expiry callback on the authority to be invoked and
   489  	// verify that the watch expired for the expected resource name and type.
   490  	eventHandler := xc.eventHandler.(*testEventHandler)
   491  	gotTyp, gotName, err := eventHandler.waitForResourceDoesNotExist(ctx)
   492  	if err != nil {
   493  		t.Fatal("Timeout when waiting for the watch expiry callback to be invoked on the xDS client")
   494  	}
   495  
   496  	if gotTyp != listenerType {
   497  		t.Fatalf("Got type %v, want %v", gotTyp, listenerType)
   498  	}
   499  	if gotName != listenerName {
   500  		t.Fatalf("Got name %v, want %v", gotName, listenerName)
   501  	}
   502  }
   503  
   504  // Tests that the xdsChannel correctly handles stream failures by ensuring that
   505  // the stream failure callback is invoked on the event handler.
   506  func (s) TestChannel_ADS_StreamFailure(t *testing.T) {
   507  	ctx, cancel := context.WithTimeout(context.Background(), 20000*defaultTestTimeout)
   508  	defer cancel()
   509  
   510  	// Start an xDS management server with a restartable listener to simulate
   511  	// connection failures.
   512  	l, err := net.Listen("tcp", "localhost:0")
   513  	if err != nil {
   514  		t.Fatalf("net.Listen() failed: %v", err)
   515  	}
   516  	lis := testutils.NewRestartableListener(l)
   517  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis})
   518  
   519  	// Configure a listener resource on the management server.
   520  	const listenerResourceName = "test-listener-resource"
   521  	const routeConfigurationName = "test-route-configuration-resource"
   522  	nodeID := uuid.New().String()
   523  	resources := e2e.UpdateOptions{
   524  		NodeID:         nodeID,
   525  		Listeners:      []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, routeConfigurationName)},
   526  		SkipValidation: true,
   527  	}
   528  	if err := mgmtServer.Update(ctx, resources); err != nil {
   529  		t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err)
   530  	}
   531  
   532  	// Create an xdsChannel for the test with a long watch expiry timer
   533  	// to ensure that watches don't expire for the duration of the test.
   534  	xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2000*defaultTestTimeout)
   535  	defer xc.close()
   536  
   537  	// Subscribe to the resource created above.
   538  	xc.subscribe(listenerType, listenerResourceName)
   539  
   540  	// Wait for an update callback on the event handler and verify the
   541  	// contents of the update and the metadata.
   542  	hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   543  		RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{
   544  			ConfigSource: &v3corepb.ConfigSource{
   545  				ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
   546  			},
   547  			RouteConfigName: routeConfigurationName,
   548  		}},
   549  	})
   550  	listenerResource, err := anypb.New(&v3listenerpb.Listener{
   551  		Name:        listenerResourceName,
   552  		ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
   553  	})
   554  	if err != nil {
   555  		t.Fatalf("Failed to create listener resource: %v", err)
   556  	}
   557  
   558  	wantUpdates := map[string]dataAndErrTuple{
   559  		listenerResourceName: {
   560  			Resource: &listenerResourceData{
   561  				Resource: listenerUpdate{
   562  					RouteConfigName: routeConfigurationName,
   563  					Raw:             listenerResource.GetValue(),
   564  				},
   565  			},
   566  		},
   567  	}
   568  	wantMD := xdsresource.UpdateMetadata{
   569  		Status:  xdsresource.ServiceStatusACKed,
   570  		Version: "1",
   571  	}
   572  
   573  	eventHandler := xc.eventHandler.(*testEventHandler)
   574  	verifyUpdateAndMetadata(ctx, t, eventHandler, wantUpdates, wantMD)
   575  
   576  	lis.Stop()
   577  	if err := eventHandler.waitForStreamFailure(ctx); err != nil {
   578  		t.Fatalf("Timeout when waiting for the stream failure callback to be invoked on the xDS client: %v", err)
   579  	}
   580  }
   581  
   582  // Tests the behavior of the xdsChannel when a resource is unsubscribed.
   583  // Verifies that when a previously subscribed resource is unsubscribed, a
   584  // request is sent without the previously subscribed resource name.
   585  func (s) TestChannel_ADS_ResourceUnsubscribe(t *testing.T) {
   586  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   587  	defer cancel()
   588  
   589  	// Start an xDS management server that uses a channel to inform the test
   590  	// about the specific LDS resource names being requested.
   591  	ldsResourcesCh := make(chan []string, 1)
   592  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   593  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   594  			t.Logf("Received request for resources: %v of type %s", req.GetResourceNames(), req.GetTypeUrl())
   595  
   596  			if req.TypeUrl != xdsresource.V3ListenerURL {
   597  				return fmt.Errorf("unexpected resource type URL: %q", req.TypeUrl)
   598  			}
   599  
   600  			// Make the most recently requested names available to the test.
   601  			ldsResourcesCh <- req.GetResourceNames()
   602  			return nil
   603  		},
   604  	})
   605  
   606  	// Configure two listener resources on the management server.
   607  	const listenerResourceName1 = "test-listener-resource-1"
   608  	const routeConfigurationName1 = "test-route-configuration-resource-1"
   609  	const listenerResourceName2 = "test-listener-resource-2"
   610  	const routeConfigurationName2 = "test-route-configuration-resource-2"
   611  	nodeID := uuid.New().String()
   612  	resources := e2e.UpdateOptions{
   613  		NodeID: nodeID,
   614  		Listeners: []*v3listenerpb.Listener{
   615  			e2e.DefaultClientListener(listenerResourceName1, routeConfigurationName1),
   616  			e2e.DefaultClientListener(listenerResourceName2, routeConfigurationName2),
   617  		},
   618  		SkipValidation: true,
   619  	}
   620  	if err := mgmtServer.Update(ctx, resources); err != nil {
   621  		t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err)
   622  	}
   623  
   624  	// Create an xdsChannel for the test with a long watch expiry timer
   625  	// to ensure that watches don't expire for the duration of the test.
   626  	xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout)
   627  	defer xc.close()
   628  
   629  	// Subscribe to the resources created above and verify that a request is
   630  	// sent for the same.
   631  	xc.subscribe(listenerType, listenerResourceName1)
   632  	xc.subscribe(listenerType, listenerResourceName2)
   633  	if err := waitForResourceNames(ctx, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil {
   634  		t.Fatal(err)
   635  	}
   636  
   637  	// Wait for the above resources to be ACKed.
   638  	if err := waitForResourceNames(ctx, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil {
   639  		t.Fatal(err)
   640  	}
   641  
   642  	// Unsubscribe to one of the resources created above, and ensure that the
   643  	// other resource is still being requested.
   644  	xc.unsubscribe(listenerType, listenerResourceName1)
   645  	if err := waitForResourceNames(ctx, ldsResourcesCh, []string{listenerResourceName2}); err != nil {
   646  		t.Fatal(err)
   647  	}
   648  
   649  	// Since the version on the management server for the above resource is not
   650  	// changed, we will not receive an update from it for the one resource that
   651  	// we are still requesting.
   652  
   653  	// Unsubscribe to the remaining resource, and ensure that no more resources
   654  	// are being requested.
   655  	xc.unsubscribe(listenerType, listenerResourceName2)
   656  	if err := waitForResourceNames(ctx, ldsResourcesCh, []string{}); err != nil {
   657  		t.Fatal(err)
   658  	}
   659  }
   660  
   661  // waitForResourceNames waits for the wantNames to be received on namesCh.
   662  // Returns a non-nil error if the context expires before that.
   663  func waitForResourceNames(ctx context.Context, namesCh chan []string, wantNames []string) error {
   664  	var lastRequestedNames []string
   665  	for ; ; <-time.After(defaultTestShortTimeout) {
   666  		select {
   667  		case <-ctx.Done():
   668  			return fmt.Errorf("timeout waiting for resources %v to be requested from the management server. Last requested resources: %v", wantNames, lastRequestedNames)
   669  		case gotNames := <-namesCh:
   670  			if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s1, s2 string) bool { return s1 < s2 })) {
   671  				return nil
   672  			}
   673  			lastRequestedNames = gotNames
   674  		}
   675  	}
   676  }
   677  
   678  // newTestEventHandler creates a new testEventHandler instance with the
   679  // necessary channels for testing the xdsChannel.
   680  func newTestEventHandler() *testEventHandler {
   681  	return &testEventHandler{
   682  		typeCh:    make(chan ResourceType, 1),
   683  		updateCh:  make(chan map[string]dataAndErrTuple, 1),
   684  		mdCh:      make(chan xdsresource.UpdateMetadata, 1),
   685  		nameCh:    make(chan string, 1),
   686  		connErrCh: make(chan error, 1),
   687  	}
   688  }
   689  
   690  // testEventHandler is a struct that implements the xdsChannelEventhandler
   691  // interface.  It is used to receive events from an xdsChannel, and has multiple
   692  // channels on which it makes these events available to the test.
   693  type testEventHandler struct {
   694  	typeCh    chan ResourceType               // Resource type of an update or resource-does-not-exist error.
   695  	updateCh  chan map[string]dataAndErrTuple // Resource updates.
   696  	mdCh      chan xdsresource.UpdateMetadata // Metadata from an update.
   697  	nameCh    chan string                     // Name of the non-existent resource.
   698  	connErrCh chan error                      // Connectivity error.
   699  
   700  }
   701  
   702  func (ta *testEventHandler) adsStreamFailure(err error) {
   703  	ta.connErrCh <- err
   704  }
   705  
   706  func (ta *testEventHandler) waitForStreamFailure(ctx context.Context) error {
   707  	select {
   708  	case <-ctx.Done():
   709  		return ctx.Err()
   710  	case <-ta.connErrCh:
   711  	}
   712  	return nil
   713  }
   714  
   715  func (ta *testEventHandler) adsResourceUpdate(typ ResourceType, updates map[string]dataAndErrTuple, md xdsresource.UpdateMetadata, onDone func()) {
   716  	ta.typeCh <- typ
   717  	ta.updateCh <- updates
   718  	ta.mdCh <- md
   719  	onDone()
   720  }
   721  
   722  // waitForUpdate waits for the next resource update event from the xdsChannel.
   723  // It returns the resource type, the resource updates, and the update metadata.
   724  // If the context is canceled, it returns an error.
   725  func (ta *testEventHandler) waitForUpdate(ctx context.Context) (ResourceType, map[string]dataAndErrTuple, xdsresource.UpdateMetadata, error) {
   726  	var typ ResourceType
   727  	var updates map[string]dataAndErrTuple
   728  	var md xdsresource.UpdateMetadata
   729  
   730  	select {
   731  	case typ = <-ta.typeCh:
   732  	case <-ctx.Done():
   733  		return ResourceType{}, nil, xdsresource.UpdateMetadata{}, ctx.Err()
   734  	}
   735  
   736  	select {
   737  	case updates = <-ta.updateCh:
   738  	case <-ctx.Done():
   739  		return ResourceType{}, nil, xdsresource.UpdateMetadata{}, ctx.Err()
   740  	}
   741  
   742  	select {
   743  	case md = <-ta.mdCh:
   744  	case <-ctx.Done():
   745  		return ResourceType{}, nil, xdsresource.UpdateMetadata{}, ctx.Err()
   746  	}
   747  	return typ, updates, md, nil
   748  }
   749  
   750  func (ta *testEventHandler) adsResourceDoesNotExist(typ ResourceType, name string) {
   751  	ta.typeCh <- typ
   752  	ta.nameCh <- name
   753  }
   754  
   755  // waitForResourceDoesNotExist waits for the next resource-does-not-exist event
   756  // from the xdsChannel. It returns the resource type and the resource name. If
   757  // the context is canceled, it returns an error.
   758  func (ta *testEventHandler) waitForResourceDoesNotExist(ctx context.Context) (ResourceType, string, error) {
   759  	var typ ResourceType
   760  	var name string
   761  
   762  	select {
   763  	case typ = <-ta.typeCh:
   764  	case <-ctx.Done():
   765  		return ResourceType{}, "", ctx.Err()
   766  	}
   767  
   768  	select {
   769  	case name = <-ta.nameCh:
   770  	case <-ctx.Done():
   771  		return ResourceType{}, "", ctx.Err()
   772  	}
   773  	return typ, name, nil
   774  }