From 07f80469728c77ef1a0e6f0f3328c7b38a66b678 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 11 Jun 2025 12:34:30 -0600 Subject: [PATCH 1/3] feat(option/internaloption): add experimental function ParseClientOptions --- .../internaloption/parsed_client_options.go | 129 ++++++++++++++++++ .../parsed_client_options_test.go | 45 ++++++ 2 files changed, 174 insertions(+) create mode 100644 option/internaloption/parsed_client_options.go create mode 100644 option/internaloption/parsed_client_options_test.go diff --git a/option/internaloption/parsed_client_options.go b/option/internaloption/parsed_client_options.go new file mode 100644 index 00000000000..70a626930ec --- /dev/null +++ b/option/internaloption/parsed_client_options.go @@ -0,0 +1,129 @@ +// Copyright 2025 Google LLC. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package internaloption contains options used internally by Google client code. +package internaloption + +import ( + "crypto/tls" + "log/slog" + "net/http" + + "cloud.google.com/go/auth" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/internal" + "google.golang.org/api/option" + "google.golang.org/grpc" +) + +// ParsedClientOptions provides read-only access to settings derived from a list of ClientOption. +// It is intended for use by other Google Cloud client libraries that accept google.golang.org/api/option.ClientOption +// and need to inspect the configured values. +type ParsedClientOptions struct { + Endpoint string + Scopes []string + TokenSource oauth2.TokenSource + Credentials *google.Credentials // From WithCredentials + CredentialsFile string + CredentialsJSON []byte + UserAgent string + APIKey string + Audiences []string + HTTPClient *http.Client + GRPCDialOpts []grpc.DialOption + GRPCConn *grpc.ClientConn + GRPCConnPoolSize int + NoAuth bool + TelemetryDisabled bool + ClientCertSource func(*tls.CertificateRequestInfo) (*tls.Certificate, error) + Impersonation *PublicImpersonationConfig + UniverseDomain string // Resolved universe domain + Logger *slog.Logger + QuotaProject string + RequestReason string + AuthCredentials *auth.Credentials // From WithAuthCredentials +} + +// PublicImpersonationConfig holds publicly accessible configuration for service account impersonation. +// It mirrors the relevant parts of the internal impersonation configuration. +type PublicImpersonationConfig struct { + TargetServiceAccount string + Delegates []string +} + +// ParseClientOptions applies the given ClientOptions, validates them, and returns +// a ParsedClientOptions struct containing the resolved settings. +// It returns an error if the provided options are invalid according to this library's validation rules. +// This function allows external libraries to inspect configuration values +// set by users through ClientOptions without needing access to internal types. +func ParseClientOptions(opts ...option.ClientOption) (*ParsedClientOptions, error) { + var ds internal.DialSettings + // Apply all options to the internal DialSettings struct. + for _, opt := range opts { + opt.Apply(&ds) + } + + // Validate the combined settings. + if err := ds.Validate(); err != nil { + return nil, err + } + + // Populate the public ParsedClientOptions struct from the internal DialSettings. + // Ensure copies are made for mutable types like slices to prevent external modification + // of any shared internal state (though ds here is local, it's good practice). + po := &ParsedClientOptions{ + Endpoint: ds.Endpoint, + UserAgent: ds.UserAgent, + APIKey: ds.APIKey, + CredentialsFile: ds.CredentialsFile, + TokenSource: ds.TokenSource, + Credentials: ds.Credentials, + HTTPClient: ds.HTTPClient, + GRPCConn: ds.GRPCConn, + GRPCConnPoolSize: ds.GRPCConnPoolSize, + NoAuth: ds.NoAuth, + TelemetryDisabled: ds.TelemetryDisabled, + ClientCertSource: ds.ClientCertSource, + UniverseDomain: ds.GetUniverseDomain(), // Uses the getter for correct precedence. + Logger: ds.Logger, + QuotaProject: ds.QuotaProject, + RequestReason: ds.RequestReason, + AuthCredentials: ds.AuthCredentials, + } + + if len(ds.CredentialsJSON) > 0 { + po.CredentialsJSON = make([]byte, len(ds.CredentialsJSON)) + copy(po.CredentialsJSON, ds.CredentialsJSON) + } + + // ds.GetScopes() returns the effective scopes (user-provided or default). + resolvedScopes := ds.GetScopes() + if len(resolvedScopes) > 0 { + po.Scopes = make([]string, len(resolvedScopes)) + copy(po.Scopes, resolvedScopes) + } + + if len(ds.Audiences) > 0 { // ds.Audiences is already a copy from withAudiences.Apply + po.Audiences = make([]string, len(ds.Audiences)) + copy(po.Audiences, ds.Audiences) + } + + if len(ds.GRPCDialOpts) > 0 { + po.GRPCDialOpts = make([]grpc.DialOption, len(ds.GRPCDialOpts)) + copy(po.GRPCDialOpts, ds.GRPCDialOpts) + } + + if ds.ImpersonationConfig != nil { + // Delegates are copied in impersonateServiceAccount.Apply + delegatesCopy := make([]string, len(ds.ImpersonationConfig.Delegates)) + copy(delegatesCopy, ds.ImpersonationConfig.Delegates) + po.Impersonation = &PublicImpersonationConfig{ + TargetServiceAccount: ds.ImpersonationConfig.Target, + Delegates: delegatesCopy, + } + } + + return po, nil +} diff --git a/option/internaloption/parsed_client_options_test.go b/option/internaloption/parsed_client_options_test.go new file mode 100644 index 00000000000..ffbfefefb21 --- /dev/null +++ b/option/internaloption/parsed_client_options_test.go @@ -0,0 +1,45 @@ +// Copyright 2021 Google LLC. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package internaloption + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/api/option" +) + +func TestParseClientOptions(t *testing.T) { + testEndpoint := "test.example.com" + testAPIKey := "testAPIKey" + testScopes := []string{"scope1", "scope2"} + + opts := []option.ClientOption{ + option.WithEndpoint(testEndpoint), + option.WithAPIKey(testAPIKey), + option.WithScopes(testScopes...), + } + + got, err := ParseClientOptions(opts...) + if err != nil { + t.Fatalf("ParseClientOptions(%v) err = %v, want nil", opts, err) + } + if got == nil { + t.Fatalf("ParseClientOptions(%v) got = nil, want non-nil", opts) + } + + if got.Endpoint != testEndpoint { + t.Errorf("ParseClientOptions().Endpoint = %q, want %q", got.Endpoint, testEndpoint) + } + if got.APIKey != testAPIKey { + t.Errorf("ParseClientOptions().APIKey = %q, want %q", got.APIKey, testAPIKey) + } + if !cmp.Equal(got.Scopes, testScopes) { + t.Errorf("ParseClientOptions().Scopes diff (-got +want):\n%s", cmp.Diff(got.Scopes, testScopes)) + } + if got.UserAgent != "" { // Default UserAgent is set by internal.DialSettings if not overridden. + // This test focuses on options passed in, not all defaults. + } +} From 83b623bd7d5d39e4fa096171e1c113efefd3e188 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 13 Jun 2025 11:00:57 -0600 Subject: [PATCH 2/3] dynamic receiver in ParseClientOptions --- .../internaloption/parsed_client_options.go | 156 ++++++++---------- .../parsed_client_options_test.go | 33 ++-- 2 files changed, 87 insertions(+), 102 deletions(-) diff --git a/option/internaloption/parsed_client_options.go b/option/internaloption/parsed_client_options.go index 70a626930ec..e827aace0d9 100644 --- a/option/internaloption/parsed_client_options.go +++ b/option/internaloption/parsed_client_options.go @@ -1,64 +1,25 @@ // Copyright 2025 Google LLC. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. - // Package internaloption contains options used internally by Google client code. package internaloption import ( - "crypto/tls" - "log/slog" - "net/http" + "reflect" - "cloud.google.com/go/auth" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" "google.golang.org/api/internal" "google.golang.org/api/option" "google.golang.org/grpc" ) -// ParsedClientOptions provides read-only access to settings derived from a list of ClientOption. -// It is intended for use by other Google Cloud client libraries that accept google.golang.org/api/option.ClientOption -// and need to inspect the configured values. -type ParsedClientOptions struct { - Endpoint string - Scopes []string - TokenSource oauth2.TokenSource - Credentials *google.Credentials // From WithCredentials - CredentialsFile string - CredentialsJSON []byte - UserAgent string - APIKey string - Audiences []string - HTTPClient *http.Client - GRPCDialOpts []grpc.DialOption - GRPCConn *grpc.ClientConn - GRPCConnPoolSize int - NoAuth bool - TelemetryDisabled bool - ClientCertSource func(*tls.CertificateRequestInfo) (*tls.Certificate, error) - Impersonation *PublicImpersonationConfig - UniverseDomain string // Resolved universe domain - Logger *slog.Logger - QuotaProject string - RequestReason string - AuthCredentials *auth.Credentials // From WithAuthCredentials -} - -// PublicImpersonationConfig holds publicly accessible configuration for service account impersonation. -// It mirrors the relevant parts of the internal impersonation configuration. -type PublicImpersonationConfig struct { - TargetServiceAccount string - Delegates []string -} - -// ParseClientOptions applies the given ClientOptions, validates them, and returns -// a ParsedClientOptions struct containing the resolved settings. -// It returns an error if the provided options are invalid according to this library's validation rules. -// This function allows external libraries to inspect configuration values -// set by users through ClientOptions without needing access to internal types. -func ParseClientOptions(opts ...option.ClientOption) (*ParsedClientOptions, error) { +// ParseClientOptions validates the given ClientOptions and updates the provided +// receiver with the resolved settings. It returns an error if the provided options +// are invalid. +// +// This function allows other Google Cloud client libraries to read configuration +// values set by users via ClientOptions, which are otherwise unreadable outside of +// google.golang.org/api. +func ParseClientOptions(receiver any, opts []option.ClientOption) error { var ds internal.DialSettings // Apply all options to the internal DialSettings struct. for _, opt := range opts { @@ -67,63 +28,82 @@ func ParseClientOptions(opts ...option.ClientOption) (*ParsedClientOptions, erro // Validate the combined settings. if err := ds.Validate(); err != nil { - return nil, err + return err } - // Populate the public ParsedClientOptions struct from the internal DialSettings. - // Ensure copies are made for mutable types like slices to prevent external modification - // of any shared internal state (though ds here is local, it's good practice). - po := &ParsedClientOptions{ - Endpoint: ds.Endpoint, - UserAgent: ds.UserAgent, - APIKey: ds.APIKey, - CredentialsFile: ds.CredentialsFile, - TokenSource: ds.TokenSource, - Credentials: ds.Credentials, - HTTPClient: ds.HTTPClient, - GRPCConn: ds.GRPCConn, - GRPCConnPoolSize: ds.GRPCConnPoolSize, - NoAuth: ds.NoAuth, - TelemetryDisabled: ds.TelemetryDisabled, - ClientCertSource: ds.ClientCertSource, - UniverseDomain: ds.GetUniverseDomain(), // Uses the getter for correct precedence. - Logger: ds.Logger, - QuotaProject: ds.QuotaProject, - RequestReason: ds.RequestReason, - AuthCredentials: ds.AuthCredentials, - } + // Populate the consumer with values from the internal DialSettings. + applyOption(receiver, "Endpoint", ds.Endpoint) + applyOption(receiver, "UserAgent", ds.UserAgent) + applyOption(receiver, "APIKey", ds.APIKey) + applyOption(receiver, "CredentialsFile", ds.CredentialsFile) + applyOption(receiver, "TokenSource", ds.TokenSource) + applyOption(receiver, "Credentials", ds.Credentials) + applyOption(receiver, "HTTPClient", ds.HTTPClient) + applyOption(receiver, "GRPCConn", ds.GRPCConn) + applyOption(receiver, "GRPCConnPoolSize", ds.GRPCConnPoolSize) + applyOption(receiver, "NoAuth", ds.NoAuth) + applyOption(receiver, "TelemetryDisabled", ds.TelemetryDisabled) + applyOption(receiver, "ClientCertSource", ds.ClientCertSource) + applyOption(receiver, "UniverseDomain", ds.GetUniverseDomain()) // Uses the getter for correct precedence. + applyOption(receiver, "Logger", ds.Logger) + applyOption(receiver, "QuotaProject", ds.QuotaProject) + applyOption(receiver, "RequestReason", ds.RequestReason) + applyOption(receiver, "AuthCredentials", ds.AuthCredentials) if len(ds.CredentialsJSON) > 0 { - po.CredentialsJSON = make([]byte, len(ds.CredentialsJSON)) - copy(po.CredentialsJSON, ds.CredentialsJSON) + credsJSONCopy := make([]byte, len(ds.CredentialsJSON)) + copy(credsJSONCopy, ds.CredentialsJSON) + applyOption(receiver, "CredentialsJSON", credsJSONCopy) } // ds.GetScopes() returns the effective scopes (user-provided or default). resolvedScopes := ds.GetScopes() if len(resolvedScopes) > 0 { - po.Scopes = make([]string, len(resolvedScopes)) - copy(po.Scopes, resolvedScopes) + scopesCopy := make([]string, len(resolvedScopes)) + copy(scopesCopy, resolvedScopes) + applyOption(receiver, "Scopes", scopesCopy) } - if len(ds.Audiences) > 0 { // ds.Audiences is already a copy from withAudiences.Apply - po.Audiences = make([]string, len(ds.Audiences)) - copy(po.Audiences, ds.Audiences) + if len(ds.Audiences) > 0 { // ds.Audiences is already a copy from withAudiences.ApplyClientOption + audiencesCopy := make([]string, len(ds.Audiences)) + copy(audiencesCopy, ds.Audiences) + applyOption(receiver, "Audiences", audiencesCopy) } if len(ds.GRPCDialOpts) > 0 { - po.GRPCDialOpts = make([]grpc.DialOption, len(ds.GRPCDialOpts)) - copy(po.GRPCDialOpts, ds.GRPCDialOpts) + grpcDialOptsCopy := make([]grpc.DialOption, len(ds.GRPCDialOpts)) + copy(grpcDialOptsCopy, ds.GRPCDialOpts) + applyOption(receiver, "GRPCDialOpts", grpcDialOptsCopy) } - if ds.ImpersonationConfig != nil { - // Delegates are copied in impersonateServiceAccount.Apply - delegatesCopy := make([]string, len(ds.ImpersonationConfig.Delegates)) - copy(delegatesCopy, ds.ImpersonationConfig.Delegates) - po.Impersonation = &PublicImpersonationConfig{ - TargetServiceAccount: ds.ImpersonationConfig.Target, - Delegates: delegatesCopy, + return nil +} + +// ApplyOption accesses the field identified by key on consumer and sets +// it to value using reflection. If the field does not exist or the value +// is not assignable, it's a no-op. +func applyOption(receiver any, key string, value any) { + v := reflect.ValueOf(receiver).Elem() + field := v.FieldByName(key) + + if !field.IsValid() || !field.CanSet() { + return // Field doesn't exist or cannot be set + } + + val := reflect.ValueOf(value) + // Handle nil interface gracefully for pointer/interface/slice/map types + if value == nil { + if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface || field.Kind() == reflect.Slice || field.Kind() == reflect.Map || field.Kind() == reflect.Func { + field.Set(reflect.Zero(field.Type())) // Set to nil/zero value for the type + return } + // For non-pointer/interface/slice/map types, if value is nil but field is not, this would be an error. + // However, we silently skip as per requirement. + return } - return po, nil + if !val.Type().AssignableTo(field.Type()) { + return // Type mismatch + } + field.Set(val) } diff --git a/option/internaloption/parsed_client_options_test.go b/option/internaloption/parsed_client_options_test.go index ffbfefefb21..1c4595ab5ed 100644 --- a/option/internaloption/parsed_client_options_test.go +++ b/option/internaloption/parsed_client_options_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC. +// Copyright 2025 Google LLC. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -11,6 +11,13 @@ import ( "google.golang.org/api/option" ) +type testReceiver struct { + APIKey string + Endpoint string + Scopes []string + UserAgent string +} + func TestParseClientOptions(t *testing.T) { testEndpoint := "test.example.com" testAPIKey := "testAPIKey" @@ -22,24 +29,22 @@ func TestParseClientOptions(t *testing.T) { option.WithScopes(testScopes...), } - got, err := ParseClientOptions(opts...) + receiver := &testReceiver{} + err := ParseClientOptions(receiver, opts) if err != nil { - t.Fatalf("ParseClientOptions(%v) err = %v, want nil", opts, err) - } - if got == nil { - t.Fatalf("ParseClientOptions(%v) got = nil, want non-nil", opts) + t.Fatalf("ParseClientOptions(receiver, %v) err = %v, want nil", opts, err) } - if got.Endpoint != testEndpoint { - t.Errorf("ParseClientOptions().Endpoint = %q, want %q", got.Endpoint, testEndpoint) + if receiver.Endpoint != testEndpoint { + t.Errorf("receiver.Endpoint = %q, want %q", receiver.Endpoint, testEndpoint) } - if got.APIKey != testAPIKey { - t.Errorf("ParseClientOptions().APIKey = %q, want %q", got.APIKey, testAPIKey) + if receiver.APIKey != testAPIKey { + t.Errorf("receiver.APIKey = %q, want %q", receiver.APIKey, testAPIKey) } - if !cmp.Equal(got.Scopes, testScopes) { - t.Errorf("ParseClientOptions().Scopes diff (-got +want):\n%s", cmp.Diff(got.Scopes, testScopes)) + if !cmp.Equal(receiver.Scopes, testScopes) { + t.Errorf("receiver.Scopes diff (-got +want):\n%s", cmp.Diff(receiver.Scopes, testScopes)) } - if got.UserAgent != "" { // Default UserAgent is set by internal.DialSettings if not overridden. - // This test focuses on options passed in, not all defaults. + if receiver.UserAgent != "" { + t.Errorf("receiver.UserAgent != nil, got %q", receiver.UserAgent) } } From 4fdc738a9f03dac01617f1a0da7b3953411cc68d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 13 Jun 2025 11:28:20 -0600 Subject: [PATCH 3/3] replace dynamic copy with ParsedOptions struct embedding internal.DialSettings --- .../internaloption/parsed_client_options.go | 95 +++---------------- .../parsed_client_options_test.go | 29 ++---- 2 files changed, 22 insertions(+), 102 deletions(-) diff --git a/option/internaloption/parsed_client_options.go b/option/internaloption/parsed_client_options.go index e827aace0d9..f91fcb68d60 100644 --- a/option/internaloption/parsed_client_options.go +++ b/option/internaloption/parsed_client_options.go @@ -5,21 +5,22 @@ package internaloption import ( - "reflect" - "google.golang.org/api/internal" "google.golang.org/api/option" - "google.golang.org/grpc" ) -// ParseClientOptions validates the given ClientOptions and updates the provided -// receiver with the resolved settings. It returns an error if the provided options -// are invalid. +type ParsedOptions struct { + internal.DialSettings +} + +// ParseClientOptions validates the given option.ClientOption slice and returns +// ParsedOptions with the resolved settings. It returns an error if the +// provided options are invalid. // // This function allows other Google Cloud client libraries to read configuration // values set by users via ClientOptions, which are otherwise unreadable outside of // google.golang.org/api. -func ParseClientOptions(receiver any, opts []option.ClientOption) error { +func ParseClientOptions(opts []option.ClientOption) (*ParsedOptions, error) { var ds internal.DialSettings // Apply all options to the internal DialSettings struct. for _, opt := range opts { @@ -28,82 +29,10 @@ func ParseClientOptions(receiver any, opts []option.ClientOption) error { // Validate the combined settings. if err := ds.Validate(); err != nil { - return err - } - - // Populate the consumer with values from the internal DialSettings. - applyOption(receiver, "Endpoint", ds.Endpoint) - applyOption(receiver, "UserAgent", ds.UserAgent) - applyOption(receiver, "APIKey", ds.APIKey) - applyOption(receiver, "CredentialsFile", ds.CredentialsFile) - applyOption(receiver, "TokenSource", ds.TokenSource) - applyOption(receiver, "Credentials", ds.Credentials) - applyOption(receiver, "HTTPClient", ds.HTTPClient) - applyOption(receiver, "GRPCConn", ds.GRPCConn) - applyOption(receiver, "GRPCConnPoolSize", ds.GRPCConnPoolSize) - applyOption(receiver, "NoAuth", ds.NoAuth) - applyOption(receiver, "TelemetryDisabled", ds.TelemetryDisabled) - applyOption(receiver, "ClientCertSource", ds.ClientCertSource) - applyOption(receiver, "UniverseDomain", ds.GetUniverseDomain()) // Uses the getter for correct precedence. - applyOption(receiver, "Logger", ds.Logger) - applyOption(receiver, "QuotaProject", ds.QuotaProject) - applyOption(receiver, "RequestReason", ds.RequestReason) - applyOption(receiver, "AuthCredentials", ds.AuthCredentials) - - if len(ds.CredentialsJSON) > 0 { - credsJSONCopy := make([]byte, len(ds.CredentialsJSON)) - copy(credsJSONCopy, ds.CredentialsJSON) - applyOption(receiver, "CredentialsJSON", credsJSONCopy) - } - - // ds.GetScopes() returns the effective scopes (user-provided or default). - resolvedScopes := ds.GetScopes() - if len(resolvedScopes) > 0 { - scopesCopy := make([]string, len(resolvedScopes)) - copy(scopesCopy, resolvedScopes) - applyOption(receiver, "Scopes", scopesCopy) - } - - if len(ds.Audiences) > 0 { // ds.Audiences is already a copy from withAudiences.ApplyClientOption - audiencesCopy := make([]string, len(ds.Audiences)) - copy(audiencesCopy, ds.Audiences) - applyOption(receiver, "Audiences", audiencesCopy) + return nil, err } - if len(ds.GRPCDialOpts) > 0 { - grpcDialOptsCopy := make([]grpc.DialOption, len(ds.GRPCDialOpts)) - copy(grpcDialOptsCopy, ds.GRPCDialOpts) - applyOption(receiver, "GRPCDialOpts", grpcDialOptsCopy) - } - - return nil -} - -// ApplyOption accesses the field identified by key on consumer and sets -// it to value using reflection. If the field does not exist or the value -// is not assignable, it's a no-op. -func applyOption(receiver any, key string, value any) { - v := reflect.ValueOf(receiver).Elem() - field := v.FieldByName(key) - - if !field.IsValid() || !field.CanSet() { - return // Field doesn't exist or cannot be set - } - - val := reflect.ValueOf(value) - // Handle nil interface gracefully for pointer/interface/slice/map types - if value == nil { - if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface || field.Kind() == reflect.Slice || field.Kind() == reflect.Map || field.Kind() == reflect.Func { - field.Set(reflect.Zero(field.Type())) // Set to nil/zero value for the type - return - } - // For non-pointer/interface/slice/map types, if value is nil but field is not, this would be an error. - // However, we silently skip as per requirement. - return - } - - if !val.Type().AssignableTo(field.Type()) { - return // Type mismatch - } - field.Set(val) + return &ParsedOptions{ + DialSettings: ds, + }, nil } diff --git a/option/internaloption/parsed_client_options_test.go b/option/internaloption/parsed_client_options_test.go index 1c4595ab5ed..87875c4f8fe 100644 --- a/option/internaloption/parsed_client_options_test.go +++ b/option/internaloption/parsed_client_options_test.go @@ -11,13 +11,6 @@ import ( "google.golang.org/api/option" ) -type testReceiver struct { - APIKey string - Endpoint string - Scopes []string - UserAgent string -} - func TestParseClientOptions(t *testing.T) { testEndpoint := "test.example.com" testAPIKey := "testAPIKey" @@ -28,23 +21,21 @@ func TestParseClientOptions(t *testing.T) { option.WithAPIKey(testAPIKey), option.WithScopes(testScopes...), } - - receiver := &testReceiver{} - err := ParseClientOptions(receiver, opts) + po, err := ParseClientOptions(opts) if err != nil { - t.Fatalf("ParseClientOptions(receiver, %v) err = %v, want nil", opts, err) + t.Fatalf("ParseClientOptions(%v) err = %v, want nil", opts, err) } - if receiver.Endpoint != testEndpoint { - t.Errorf("receiver.Endpoint = %q, want %q", receiver.Endpoint, testEndpoint) + if po.Endpoint != testEndpoint { + t.Errorf("po.Endpoint = %q, want %q", po.Endpoint, testEndpoint) } - if receiver.APIKey != testAPIKey { - t.Errorf("receiver.APIKey = %q, want %q", receiver.APIKey, testAPIKey) + if po.APIKey != testAPIKey { + t.Errorf("po.APIKey = %q, want %q", po.APIKey, testAPIKey) } - if !cmp.Equal(receiver.Scopes, testScopes) { - t.Errorf("receiver.Scopes diff (-got +want):\n%s", cmp.Diff(receiver.Scopes, testScopes)) + if !cmp.Equal(po.Scopes, testScopes) { + t.Errorf("po.Scopes diff (-got +want):\n%s", cmp.Diff(po.Scopes, testScopes)) } - if receiver.UserAgent != "" { - t.Errorf("receiver.UserAgent != nil, got %q", receiver.UserAgent) + if po.UserAgent != "" { + t.Errorf("po.UserAgent != nil, got %q", po.UserAgent) } }