...

Source file src/cloud.google.com/go/storage/bucket_test.go

Documentation: cloud.google.com/go/storage

     1  // Copyright 2017 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package storage
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"cloud.google.com/go/internal/testutil"
    24  	"cloud.google.com/go/storage/internal/apiv2/storagepb"
    25  	"github.com/google/go-cmp/cmp"
    26  	gax "github.com/googleapis/gax-go/v2"
    27  	"golang.org/x/oauth2/google"
    28  	"google.golang.org/api/googleapi"
    29  	"google.golang.org/api/option"
    30  	raw "google.golang.org/api/storage/v1"
    31  	"google.golang.org/protobuf/proto"
    32  	"google.golang.org/protobuf/types/known/durationpb"
    33  )
    34  
    35  func TestBucketAttrsToRawBucket(t *testing.T) {
    36  	t.Parallel()
    37  	attrs := &BucketAttrs{
    38  		Name: "name",
    39  		ACL:  []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}},
    40  		DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid",
    41  			ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}},
    42  		Etag:         "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
    43  		Location:     "loc",
    44  		StorageClass: "class",
    45  		RetentionPolicy: &RetentionPolicy{
    46  			RetentionPeriod: 3 * time.Second,
    47  		},
    48  		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true},
    49  		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
    50  		PublicAccessPrevention:   PublicAccessPreventionEnforced,
    51  		VersioningEnabled:        false,
    52  		RPO:                      RPOAsyncTurbo,
    53  		// should be ignored:
    54  		MetaGeneration: 39,
    55  		Created:        time.Now(),
    56  		Labels:         map[string]string{"label": "value"},
    57  		CORS: []CORS{
    58  			{
    59  				MaxAge:          time.Hour,
    60  				Methods:         []string{"GET", "POST"},
    61  				Origins:         []string{"*"},
    62  				ResponseHeaders: []string{"FOO"},
    63  			},
    64  		},
    65  		Encryption:       &BucketEncryption{DefaultKMSKeyName: "key"},
    66  		Logging:          &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
    67  		Website:          &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
    68  		Autoclass:        &Autoclass{Enabled: true, TerminalStorageClass: "NEARLINE"},
    69  		SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: time.Hour},
    70  		Lifecycle: Lifecycle{
    71  			Rules: []LifecycleRule{{
    72  				Action: LifecycleAction{
    73  					Type:         SetStorageClassAction,
    74  					StorageClass: "NEARLINE",
    75  				},
    76  				Condition: LifecycleCondition{
    77  					AgeInDays:             10,
    78  					Liveness:              Live,
    79  					CreatedBefore:         time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
    80  					MatchesStorageClasses: []string{"STANDARD"},
    81  					NumNewerVersions:      3,
    82  				},
    83  			}, {
    84  				Action: LifecycleAction{
    85  					Type:         SetStorageClassAction,
    86  					StorageClass: "ARCHIVE",
    87  				},
    88  				Condition: LifecycleCondition{
    89  					CustomTimeBefore:      time.Date(2020, 1, 2, 3, 0, 0, 0, time.UTC),
    90  					DaysSinceCustomTime:   100,
    91  					Liveness:              Live,
    92  					MatchesStorageClasses: []string{"STANDARD"},
    93  				},
    94  			}, {
    95  				Action: LifecycleAction{
    96  					Type: DeleteAction,
    97  				},
    98  				Condition: LifecycleCondition{
    99  					DaysSinceNoncurrentTime: 30,
   100  					Liveness:                Live,
   101  					NoncurrentTimeBefore:    time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
   102  					MatchesStorageClasses:   []string{"NEARLINE"},
   103  					NumNewerVersions:        10,
   104  				},
   105  			}, {
   106  				Action: LifecycleAction{
   107  					Type: DeleteAction,
   108  				},
   109  				Condition: LifecycleCondition{
   110  					AgeInDays:        10,
   111  					MatchesPrefix:    []string{"testPrefix"},
   112  					MatchesSuffix:    []string{"testSuffix"},
   113  					NumNewerVersions: 3,
   114  				},
   115  			}, {
   116  				Action: LifecycleAction{
   117  					Type: DeleteAction,
   118  				},
   119  				Condition: LifecycleCondition{
   120  					Liveness: Archived,
   121  				},
   122  			}, {
   123  				Action: LifecycleAction{
   124  					Type: AbortIncompleteMPUAction,
   125  				},
   126  				Condition: LifecycleCondition{
   127  					AgeInDays: 20,
   128  				},
   129  			}, {
   130  				Action: LifecycleAction{
   131  					Type: DeleteAction,
   132  				},
   133  				Condition: LifecycleCondition{
   134  					AllObjects: true,
   135  				},
   136  			}},
   137  		},
   138  	}
   139  	got := attrs.toRawBucket()
   140  	want := &raw.Bucket{
   141  		Name: "name",
   142  		Acl: []*raw.BucketAccessControl{
   143  			{Entity: "bob@example.com", Role: "OWNER"}, // other fields ignored on create/update
   144  		},
   145  		DefaultObjectAcl: []*raw.ObjectAccessControl{
   146  			{Entity: "allUsers", Role: "READER"}, // other fields ignored on create/update
   147  		},
   148  		Location:     "loc",
   149  		StorageClass: "class",
   150  		RetentionPolicy: &raw.BucketRetentionPolicy{
   151  			RetentionPeriod: 3,
   152  		},
   153  		IamConfiguration: &raw.BucketIamConfiguration{
   154  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   155  				Enabled: true,
   156  			},
   157  			PublicAccessPrevention: "enforced",
   158  		},
   159  		Versioning: nil, // ignore VersioningEnabled if false
   160  		Rpo:        rpoAsyncTurbo,
   161  		Labels:     map[string]string{"label": "value"},
   162  		Cors: []*raw.BucketCors{
   163  			{
   164  				MaxAgeSeconds:  3600,
   165  				Method:         []string{"GET", "POST"},
   166  				Origin:         []string{"*"},
   167  				ResponseHeader: []string{"FOO"},
   168  			},
   169  		},
   170  		Encryption:       &raw.BucketEncryption{DefaultKmsKeyName: "key"},
   171  		Logging:          &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   172  		Website:          &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   173  		Autoclass:        &raw.BucketAutoclass{Enabled: true, TerminalStorageClass: "NEARLINE"},
   174  		SoftDeletePolicy: &raw.BucketSoftDeletePolicy{RetentionDurationSeconds: 60 * 60},
   175  		Lifecycle: &raw.BucketLifecycle{
   176  			Rule: []*raw.BucketLifecycleRule{{
   177  				Action: &raw.BucketLifecycleRuleAction{
   178  					Type:         SetStorageClassAction,
   179  					StorageClass: "NEARLINE",
   180  				},
   181  				Condition: &raw.BucketLifecycleRuleCondition{
   182  					Age:                 googleapi.Int64(10),
   183  					IsLive:              googleapi.Bool(true),
   184  					CreatedBefore:       "2017-01-02",
   185  					MatchesStorageClass: []string{"STANDARD"},
   186  					NumNewerVersions:    3,
   187  				},
   188  			},
   189  				{
   190  					Action: &raw.BucketLifecycleRuleAction{
   191  						StorageClass: "ARCHIVE",
   192  						Type:         SetStorageClassAction,
   193  					},
   194  					Condition: &raw.BucketLifecycleRuleCondition{
   195  						IsLive:              googleapi.Bool(true),
   196  						CustomTimeBefore:    "2020-01-02",
   197  						DaysSinceCustomTime: 100,
   198  						MatchesStorageClass: []string{"STANDARD"},
   199  					},
   200  				},
   201  				{
   202  					Action: &raw.BucketLifecycleRuleAction{
   203  						Type: DeleteAction,
   204  					},
   205  					Condition: &raw.BucketLifecycleRuleCondition{
   206  						DaysSinceNoncurrentTime: 30,
   207  						IsLive:                  googleapi.Bool(true),
   208  						NoncurrentTimeBefore:    "2017-01-02",
   209  						MatchesStorageClass:     []string{"NEARLINE"},
   210  						NumNewerVersions:        10,
   211  					},
   212  				},
   213  				{
   214  					Action: &raw.BucketLifecycleRuleAction{
   215  						Type: DeleteAction,
   216  					},
   217  					Condition: &raw.BucketLifecycleRuleCondition{
   218  						Age:              googleapi.Int64(10),
   219  						MatchesPrefix:    []string{"testPrefix"},
   220  						MatchesSuffix:    []string{"testSuffix"},
   221  						NumNewerVersions: 3,
   222  					},
   223  				},
   224  				{
   225  					Action: &raw.BucketLifecycleRuleAction{
   226  						Type: DeleteAction,
   227  					},
   228  					Condition: &raw.BucketLifecycleRuleCondition{
   229  						IsLive: googleapi.Bool(false),
   230  					},
   231  				},
   232  				{
   233  					Action: &raw.BucketLifecycleRuleAction{
   234  						Type: AbortIncompleteMPUAction,
   235  					},
   236  					Condition: &raw.BucketLifecycleRuleCondition{
   237  						Age: googleapi.Int64(20),
   238  					},
   239  				},
   240  				{
   241  					Action: &raw.BucketLifecycleRuleAction{
   242  						Type: DeleteAction,
   243  					},
   244  					Condition: &raw.BucketLifecycleRuleCondition{
   245  						Age:             googleapi.Int64(0),
   246  						ForceSendFields: []string{"Age"},
   247  					},
   248  				},
   249  			},
   250  		},
   251  	}
   252  	if msg := testutil.Diff(got, want); msg != "" {
   253  		t.Error(msg)
   254  	}
   255  
   256  	attrs.VersioningEnabled = true
   257  	attrs.RequesterPays = true
   258  	got = attrs.toRawBucket()
   259  	want.Versioning = &raw.BucketVersioning{Enabled: true}
   260  	want.Billing = &raw.BucketBilling{RequesterPays: true}
   261  	if msg := testutil.Diff(got, want); msg != "" {
   262  		t.Error(msg)
   263  	}
   264  
   265  	// Test that setting either of BucketPolicyOnly or UniformBucketLevelAccess
   266  	// will enable UniformBucketLevelAccess.
   267  	// Set UBLA.Enabled = true --> UBLA should be set to enabled in the proto.
   268  	attrs.BucketPolicyOnly = BucketPolicyOnly{}
   269  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
   270  	got = attrs.toRawBucket()
   271  	want.IamConfiguration = &raw.BucketIamConfiguration{
   272  		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   273  			Enabled: true,
   274  		},
   275  		PublicAccessPrevention: "enforced",
   276  	}
   277  	if msg := testutil.Diff(got, want); msg != "" {
   278  		t.Errorf(msg)
   279  	}
   280  
   281  	// Set BucketPolicyOnly.Enabled = true --> UBLA should be set to enabled in
   282  	// the proto.
   283  	attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true}
   284  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{}
   285  	got = attrs.toRawBucket()
   286  	want.IamConfiguration = &raw.BucketIamConfiguration{
   287  		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   288  			Enabled: true,
   289  		},
   290  		PublicAccessPrevention: "enforced",
   291  	}
   292  	if msg := testutil.Diff(got, want); msg != "" {
   293  		t.Errorf(msg)
   294  	}
   295  
   296  	// Set both BucketPolicyOnly.Enabled = true and
   297  	// UniformBucketLevelAccess.Enabled=true --> UBLA should be set to enabled
   298  	// in the proto.
   299  	attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true}
   300  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
   301  	got = attrs.toRawBucket()
   302  	want.IamConfiguration = &raw.BucketIamConfiguration{
   303  		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   304  			Enabled: true,
   305  		},
   306  		PublicAccessPrevention: "enforced",
   307  	}
   308  	if msg := testutil.Diff(got, want); msg != "" {
   309  		t.Errorf(msg)
   310  	}
   311  
   312  	// Set UBLA.Enabled=false and BucketPolicyOnly.Enabled=false --> UBLA
   313  	// should be disabled in the proto.
   314  	attrs.BucketPolicyOnly = BucketPolicyOnly{}
   315  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{}
   316  	got = attrs.toRawBucket()
   317  	want.IamConfiguration = &raw.BucketIamConfiguration{
   318  		PublicAccessPrevention: "enforced",
   319  	}
   320  	if msg := testutil.Diff(got, want); msg != "" {
   321  		t.Errorf(msg)
   322  	}
   323  
   324  	// Test that setting PublicAccessPrevention to "unspecified" leads to the
   325  	// inherited setting being propagated in the proto.
   326  	attrs.PublicAccessPrevention = PublicAccessPreventionUnspecified
   327  	got = attrs.toRawBucket()
   328  	want.IamConfiguration = &raw.BucketIamConfiguration{
   329  		PublicAccessPrevention: "inherited",
   330  	}
   331  	if msg := testutil.Diff(got, want); msg != "" {
   332  		t.Errorf(msg)
   333  	}
   334  
   335  	// Test that setting PublicAccessPrevention to "inherited" leads to the
   336  	// setting being propagated in the proto.
   337  	attrs.PublicAccessPrevention = PublicAccessPreventionInherited
   338  	got = attrs.toRawBucket()
   339  	want.IamConfiguration = &raw.BucketIamConfiguration{
   340  		PublicAccessPrevention: "inherited",
   341  	}
   342  	if msg := testutil.Diff(got, want); msg != "" {
   343  		t.Errorf(msg)
   344  	}
   345  
   346  	// Test that setting RPO to default is propagated in the proto.
   347  	attrs.RPO = RPODefault
   348  	got = attrs.toRawBucket()
   349  	want.Rpo = rpoDefault
   350  	if msg := testutil.Diff(got, want); msg != "" {
   351  		t.Errorf(msg)
   352  	}
   353  
   354  	// Re-enable UBLA and confirm that it does not affect the PAP setting.
   355  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
   356  	got = attrs.toRawBucket()
   357  	want.IamConfiguration = &raw.BucketIamConfiguration{
   358  		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   359  			Enabled: true,
   360  		},
   361  		PublicAccessPrevention: "inherited",
   362  	}
   363  	if msg := testutil.Diff(got, want); msg != "" {
   364  		t.Errorf(msg)
   365  	}
   366  
   367  	// Disable UBLA and reset PAP to default. Confirm that the IAM config is set
   368  	// to nil in the proto.
   369  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: false}
   370  	attrs.PublicAccessPrevention = PublicAccessPreventionUnknown
   371  	got = attrs.toRawBucket()
   372  	want.IamConfiguration = nil
   373  	if msg := testutil.Diff(got, want); msg != "" {
   374  		t.Errorf(msg)
   375  	}
   376  }
   377  
   378  func TestBucketAttrsToUpdateToRawBucket(t *testing.T) {
   379  	t.Parallel()
   380  	au := &BucketAttrsToUpdate{
   381  		VersioningEnabled:        false,
   382  		RequesterPays:            false,
   383  		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: false},
   384  		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false},
   385  		DefaultEventBasedHold:    false,
   386  		RetentionPolicy:          &RetentionPolicy{RetentionPeriod: time.Hour},
   387  		Encryption:               &BucketEncryption{DefaultKMSKeyName: "key2"},
   388  		Lifecycle: &Lifecycle{
   389  			Rules: []LifecycleRule{
   390  				{
   391  					Action:    LifecycleAction{Type: "Delete"},
   392  					Condition: LifecycleCondition{AgeInDays: 30},
   393  				},
   394  				{
   395  					Action:    LifecycleAction{Type: AbortIncompleteMPUAction},
   396  					Condition: LifecycleCondition{AgeInDays: 13},
   397  				},
   398  			},
   399  		},
   400  		Logging:          &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   401  		Website:          &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   402  		StorageClass:     "NEARLINE",
   403  		Autoclass:        &Autoclass{Enabled: true, TerminalStorageClass: "ARCHIVE"},
   404  		SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: time.Hour},
   405  	}
   406  	au.SetLabel("a", "foo")
   407  	au.DeleteLabel("b")
   408  	au.SetLabel("c", "")
   409  	got := au.toRawBucket()
   410  	want := &raw.Bucket{
   411  		Versioning: &raw.BucketVersioning{
   412  			Enabled:         false,
   413  			ForceSendFields: []string{"Enabled"},
   414  		},
   415  		Labels: map[string]string{
   416  			"a": "foo",
   417  			"c": "",
   418  		},
   419  		Billing: &raw.BucketBilling{
   420  			RequesterPays:   false,
   421  			ForceSendFields: []string{"RequesterPays"},
   422  		},
   423  		DefaultEventBasedHold: false,
   424  		RetentionPolicy:       &raw.BucketRetentionPolicy{RetentionPeriod: 3600},
   425  		IamConfiguration: &raw.BucketIamConfiguration{
   426  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   427  				Enabled:         false,
   428  				ForceSendFields: []string{"Enabled"},
   429  			},
   430  		},
   431  		Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key2"},
   432  		NullFields: []string{"Labels.b"},
   433  		Lifecycle: &raw.BucketLifecycle{
   434  			Rule: []*raw.BucketLifecycleRule{
   435  				{
   436  					Action:    &raw.BucketLifecycleRuleAction{Type: "Delete"},
   437  					Condition: &raw.BucketLifecycleRuleCondition{Age: googleapi.Int64(30)},
   438  				},
   439  				{
   440  					Action:    &raw.BucketLifecycleRuleAction{Type: AbortIncompleteMPUAction},
   441  					Condition: &raw.BucketLifecycleRuleCondition{Age: googleapi.Int64(13)},
   442  				},
   443  			},
   444  		},
   445  		Logging:          &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   446  		Website:          &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   447  		StorageClass:     "NEARLINE",
   448  		Autoclass:        &raw.BucketAutoclass{Enabled: true, TerminalStorageClass: "ARCHIVE", ForceSendFields: []string{"Enabled"}},
   449  		SoftDeletePolicy: &raw.BucketSoftDeletePolicy{RetentionDurationSeconds: 3600},
   450  		ForceSendFields:  []string{"DefaultEventBasedHold", "Lifecycle", "Autoclass"},
   451  	}
   452  	if msg := testutil.Diff(got, want); msg != "" {
   453  		t.Error(msg)
   454  	}
   455  
   456  	var au2 BucketAttrsToUpdate
   457  	au2.DeleteLabel("b")
   458  	got = au2.toRawBucket()
   459  	want = &raw.Bucket{
   460  		Labels:          map[string]string{},
   461  		ForceSendFields: []string{"Labels"},
   462  		NullFields:      []string{"Labels.b"},
   463  	}
   464  	if msg := testutil.Diff(got, want); msg != "" {
   465  		t.Error(msg)
   466  	}
   467  
   468  	// Test nulls.
   469  	au3 := &BucketAttrsToUpdate{
   470  		RetentionPolicy:  &RetentionPolicy{},
   471  		Encryption:       &BucketEncryption{},
   472  		Logging:          &BucketLogging{},
   473  		Website:          &BucketWebsite{},
   474  		SoftDeletePolicy: &SoftDeletePolicy{},
   475  	}
   476  	got = au3.toRawBucket()
   477  	want = &raw.Bucket{
   478  		NullFields: []string{"RetentionPolicy", "Encryption", "Logging", "Website", "SoftDeletePolicy"},
   479  	}
   480  	if msg := testutil.Diff(got, want); msg != "" {
   481  		t.Error(msg)
   482  	}
   483  
   484  	// Test that setting either of BucketPolicyOnly or UniformBucketLevelAccess
   485  	// will enable UniformBucketLevelAccess.
   486  	// Set UBLA.Enabled = true --> UBLA should be set to enabled in the proto.
   487  	au4 := &BucketAttrsToUpdate{
   488  		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: true},
   489  	}
   490  	got = au4.toRawBucket()
   491  	want = &raw.Bucket{
   492  		IamConfiguration: &raw.BucketIamConfiguration{
   493  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   494  				Enabled:         true,
   495  				ForceSendFields: []string{"Enabled"},
   496  			},
   497  		},
   498  	}
   499  	if msg := testutil.Diff(got, want); msg != "" {
   500  		t.Errorf(msg)
   501  	}
   502  
   503  	// Set BucketPolicyOnly.Enabled = true --> UBLA should be set to enabled in
   504  	// the proto.
   505  	au5 := &BucketAttrsToUpdate{
   506  		BucketPolicyOnly: &BucketPolicyOnly{Enabled: true},
   507  	}
   508  	got = au5.toRawBucket()
   509  	want = &raw.Bucket{
   510  		IamConfiguration: &raw.BucketIamConfiguration{
   511  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   512  				Enabled:         true,
   513  				ForceSendFields: []string{"Enabled"},
   514  			},
   515  		},
   516  	}
   517  	if msg := testutil.Diff(got, want); msg != "" {
   518  		t.Errorf(msg)
   519  	}
   520  
   521  	// Set both BucketPolicyOnly.Enabled = true and
   522  	// UniformBucketLevelAccess.Enabled=true --> UBLA should be set to enabled
   523  	// in the proto.
   524  	au6 := &BucketAttrsToUpdate{
   525  		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: true},
   526  		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: true},
   527  	}
   528  	got = au6.toRawBucket()
   529  	want = &raw.Bucket{
   530  		IamConfiguration: &raw.BucketIamConfiguration{
   531  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   532  				Enabled:         true,
   533  				ForceSendFields: []string{"Enabled"},
   534  			},
   535  		},
   536  	}
   537  	if msg := testutil.Diff(got, want); msg != "" {
   538  		t.Errorf(msg)
   539  	}
   540  
   541  	// Set UBLA.Enabled=false and BucketPolicyOnly.Enabled=false --> UBLA
   542  	// should be disabled in the proto.
   543  	au7 := &BucketAttrsToUpdate{
   544  		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: false},
   545  		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false},
   546  	}
   547  	got = au7.toRawBucket()
   548  	want = &raw.Bucket{
   549  		IamConfiguration: &raw.BucketIamConfiguration{
   550  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   551  				Enabled:         false,
   552  				ForceSendFields: []string{"Enabled"},
   553  			},
   554  		},
   555  	}
   556  	if msg := testutil.Diff(got, want); msg != "" {
   557  		t.Errorf(msg)
   558  	}
   559  
   560  	// UBLA.Enabled will have precedence above BucketPolicyOnly.Enabled if both
   561  	// are set with different values.
   562  	au8 := &BucketAttrsToUpdate{
   563  		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: true},
   564  		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false},
   565  	}
   566  	got = au8.toRawBucket()
   567  	want = &raw.Bucket{
   568  		IamConfiguration: &raw.BucketIamConfiguration{
   569  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   570  				Enabled:         false,
   571  				ForceSendFields: []string{"Enabled"},
   572  			},
   573  		},
   574  	}
   575  	if msg := testutil.Diff(got, want); msg != "" {
   576  		t.Errorf(msg)
   577  	}
   578  
   579  	// Set an empty Lifecycle and verify that it will be sent.
   580  	au9 := &BucketAttrsToUpdate{
   581  		Lifecycle: &Lifecycle{},
   582  	}
   583  	got = au9.toRawBucket()
   584  	want = &raw.Bucket{
   585  		Lifecycle: &raw.BucketLifecycle{
   586  			ForceSendFields: []string{"Rule"},
   587  		},
   588  		ForceSendFields: []string{"Lifecycle"},
   589  	}
   590  	if msg := testutil.Diff(got, want); msg != "" {
   591  		t.Errorf(msg)
   592  	}
   593  }
   594  
   595  func TestNewBucket(t *testing.T) {
   596  	labels := map[string]string{"a": "b"}
   597  	matchClasses := []string{"STANDARD"}
   598  	aTime := time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC)
   599  	rb := &raw.Bucket{
   600  		Name:                  "name",
   601  		Location:              "loc",
   602  		DefaultEventBasedHold: true,
   603  		Metageneration:        3,
   604  		StorageClass:          "sc",
   605  		TimeCreated:           "2017-10-23T04:05:06Z",
   606  		Versioning:            &raw.BucketVersioning{Enabled: true},
   607  		Labels:                labels,
   608  		Billing:               &raw.BucketBilling{RequesterPays: true},
   609  		Etag:                  "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
   610  		Lifecycle: &raw.BucketLifecycle{
   611  			Rule: []*raw.BucketLifecycleRule{{
   612  				Action: &raw.BucketLifecycleRuleAction{
   613  					Type:         "SetStorageClass",
   614  					StorageClass: "NEARLINE",
   615  				},
   616  				Condition: &raw.BucketLifecycleRuleCondition{
   617  					Age:                 googleapi.Int64(10),
   618  					IsLive:              googleapi.Bool(true),
   619  					CreatedBefore:       "2017-01-02",
   620  					MatchesStorageClass: matchClasses,
   621  					NumNewerVersions:    3,
   622  				},
   623  			}},
   624  		},
   625  		RetentionPolicy: &raw.BucketRetentionPolicy{
   626  			RetentionPeriod: 3,
   627  			EffectiveTime:   aTime.Format(time.RFC3339),
   628  		},
   629  		ObjectRetention: &raw.BucketObjectRetention{
   630  			Mode: "Enabled",
   631  		},
   632  		IamConfiguration: &raw.BucketIamConfiguration{
   633  			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
   634  				Enabled:    true,
   635  				LockedTime: aTime.Format(time.RFC3339),
   636  			},
   637  			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
   638  				Enabled:    true,
   639  				LockedTime: aTime.Format(time.RFC3339),
   640  			},
   641  		},
   642  		Cors: []*raw.BucketCors{
   643  			{
   644  				MaxAgeSeconds:  3600,
   645  				Method:         []string{"GET", "POST"},
   646  				Origin:         []string{"*"},
   647  				ResponseHeader: []string{"FOO"},
   648  			},
   649  		},
   650  		Acl: []*raw.BucketAccessControl{
   651  			{Bucket: "name", Role: "READER", Email: "joe@example.com", Entity: "allUsers"},
   652  		},
   653  		LocationType:  "dual-region",
   654  		Encryption:    &raw.BucketEncryption{DefaultKmsKeyName: "key"},
   655  		Logging:       &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   656  		Website:       &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   657  		ProjectNumber: 123231313,
   658  		Autoclass: &raw.BucketAutoclass{
   659  			Enabled:                        true,
   660  			ToggleTime:                     "2017-10-23T04:05:06Z",
   661  			TerminalStorageClass:           "NEARLINE",
   662  			TerminalStorageClassUpdateTime: "2017-10-23T04:05:06Z",
   663  		},
   664  		SoftDeletePolicy: &raw.BucketSoftDeletePolicy{
   665  			EffectiveTime:            "2017-10-23T04:05:06Z",
   666  			RetentionDurationSeconds: 3600,
   667  		},
   668  	}
   669  	want := &BucketAttrs{
   670  		Name:                  "name",
   671  		Location:              "loc",
   672  		DefaultEventBasedHold: true,
   673  		MetaGeneration:        3,
   674  		StorageClass:          "sc",
   675  		Created:               time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
   676  		VersioningEnabled:     true,
   677  		Labels:                labels,
   678  		Etag:                  "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
   679  		RequesterPays:         true,
   680  		Lifecycle: Lifecycle{
   681  			Rules: []LifecycleRule{
   682  				{
   683  					Action: LifecycleAction{
   684  						Type:         SetStorageClassAction,
   685  						StorageClass: "NEARLINE",
   686  					},
   687  					Condition: LifecycleCondition{
   688  						AgeInDays:             10,
   689  						Liveness:              Live,
   690  						CreatedBefore:         time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC),
   691  						MatchesStorageClasses: matchClasses,
   692  						NumNewerVersions:      3,
   693  					},
   694  				},
   695  			},
   696  		},
   697  		RetentionPolicy: &RetentionPolicy{
   698  			EffectiveTime:   aTime,
   699  			RetentionPeriod: 3 * time.Second,
   700  		},
   701  		ObjectRetentionMode:      "Enabled",
   702  		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true, LockedTime: aTime},
   703  		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: aTime},
   704  		CORS: []CORS{
   705  			{
   706  				MaxAge:          time.Hour,
   707  				Methods:         []string{"GET", "POST"},
   708  				Origins:         []string{"*"},
   709  				ResponseHeaders: []string{"FOO"},
   710  			},
   711  		},
   712  		Encryption:       &BucketEncryption{DefaultKMSKeyName: "key"},
   713  		Logging:          &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   714  		Website:          &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   715  		ACL:              []ACLRule{{Entity: "allUsers", Role: RoleReader, Email: "joe@example.com"}},
   716  		DefaultObjectACL: nil,
   717  		LocationType:     "dual-region",
   718  		ProjectNumber:    123231313,
   719  		Autoclass: &Autoclass{
   720  			Enabled:                        true,
   721  			ToggleTime:                     time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
   722  			TerminalStorageClass:           "NEARLINE",
   723  			TerminalStorageClassUpdateTime: time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
   724  		},
   725  		SoftDeletePolicy: &SoftDeletePolicy{
   726  			EffectiveTime:     time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
   727  			RetentionDuration: time.Hour,
   728  		},
   729  	}
   730  	got, err := newBucket(rb)
   731  	if err != nil {
   732  		t.Fatal(err)
   733  	}
   734  	if diff := cmp.Diff(got, want); diff != "" {
   735  		t.Errorf("got=-, want=+:\n%s", diff)
   736  	}
   737  }
   738  
   739  func TestNewBucketFromProto(t *testing.T) {
   740  	autoclassTSC := "NEARLINE"
   741  	pb := &storagepb.Bucket{
   742  		Name: "name",
   743  		Acl: []*storagepb.BucketAccessControl{
   744  			{Entity: "bob@example.com", Role: "OWNER"},
   745  		},
   746  		DefaultObjectAcl: []*storagepb.ObjectAccessControl{
   747  			{Entity: "allUsers", Role: "READER"},
   748  		},
   749  		Location:     "loc",
   750  		LocationType: "region",
   751  		StorageClass: "class",
   752  		RetentionPolicy: &storagepb.Bucket_RetentionPolicy{
   753  			RetentionDuration: durationpb.New(3 * time.Second),
   754  			EffectiveTime:     toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
   755  		},
   756  		IamConfig: &storagepb.Bucket_IamConfig{
   757  			UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
   758  				Enabled:  true,
   759  				LockTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
   760  			},
   761  			PublicAccessPrevention: "enforced",
   762  		},
   763  		Rpo:            rpoAsyncTurbo,
   764  		Metageneration: int64(39),
   765  		CreateTime:     toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
   766  		Labels:         map[string]string{"label": "value"},
   767  		Cors: []*storagepb.Bucket_Cors{
   768  			{
   769  				MaxAgeSeconds:  3600,
   770  				Method:         []string{"GET", "POST"},
   771  				Origin:         []string{"*"},
   772  				ResponseHeader: []string{"FOO"},
   773  			},
   774  		},
   775  		Encryption: &storagepb.Bucket_Encryption{DefaultKmsKey: "key"},
   776  		Logging:    &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"},
   777  		Website:    &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"},
   778  		Autoclass: &storagepb.Bucket_Autoclass{
   779  			Enabled:                        true,
   780  			ToggleTime:                     toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
   781  			TerminalStorageClass:           &autoclassTSC,
   782  			TerminalStorageClassUpdateTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
   783  		},
   784  		SoftDeletePolicy: &storagepb.Bucket_SoftDeletePolicy{
   785  			RetentionDuration: durationpb.New(3 * time.Hour),
   786  			EffectiveTime:     toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
   787  		},
   788  		Lifecycle: &storagepb.Bucket_Lifecycle{
   789  			Rule: []*storagepb.Bucket_Lifecycle_Rule{
   790  				{
   791  					Action: &storagepb.Bucket_Lifecycle_Rule_Action{Type: "Delete"},
   792  					Condition: &storagepb.Bucket_Lifecycle_Rule_Condition{
   793  						AgeDays: proto.Int32(int32(10)),
   794  					},
   795  				},
   796  			},
   797  		},
   798  	}
   799  	want := &BucketAttrs{
   800  		Name:             "name",
   801  		ACL:              []ACLRule{{Entity: "bob@example.com", Role: RoleOwner}},
   802  		DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader}},
   803  		Location:         "loc",
   804  		LocationType:     "region",
   805  		StorageClass:     "class",
   806  		RetentionPolicy: &RetentionPolicy{
   807  			RetentionPeriod: 3 * time.Second,
   808  			EffectiveTime:   time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   809  		},
   810  		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true, LockedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
   811  		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
   812  		PublicAccessPrevention:   PublicAccessPreventionEnforced,
   813  		RPO:                      RPOAsyncTurbo,
   814  		MetaGeneration:           39,
   815  		Created:                  time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   816  		Labels:                   map[string]string{"label": "value"},
   817  		CORS: []CORS{
   818  			{
   819  				MaxAge:          time.Hour,
   820  				Methods:         []string{"GET", "POST"},
   821  				Origins:         []string{"*"},
   822  				ResponseHeaders: []string{"FOO"},
   823  			},
   824  		},
   825  		Encryption: &BucketEncryption{DefaultKMSKeyName: "key"},
   826  		Logging:    &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   827  		Website:    &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   828  		Autoclass:  &Autoclass{Enabled: true, ToggleTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), TerminalStorageClass: "NEARLINE", TerminalStorageClassUpdateTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
   829  		SoftDeletePolicy: &SoftDeletePolicy{
   830  			EffectiveTime:     time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   831  			RetentionDuration: time.Hour * 3,
   832  		},
   833  		Lifecycle: Lifecycle{
   834  			Rules: []LifecycleRule{{
   835  				Action: LifecycleAction{
   836  					Type: DeleteAction,
   837  				},
   838  				Condition: LifecycleCondition{
   839  					AgeInDays: 10,
   840  				},
   841  			}},
   842  		},
   843  	}
   844  	got := newBucketFromProto(pb)
   845  	if diff := cmp.Diff(got, want); diff != "" {
   846  		t.Errorf("got=-, want=+:\n%s", diff)
   847  	}
   848  }
   849  
   850  func TestBucketAttrsToProtoBucket(t *testing.T) {
   851  	t.Parallel()
   852  	attrs := &BucketAttrs{
   853  		Name: "name",
   854  		ACL:  []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}},
   855  		DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid",
   856  			ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}},
   857  		Location:     "loc",
   858  		StorageClass: "class",
   859  		RetentionPolicy: &RetentionPolicy{
   860  			RetentionPeriod: 3 * time.Second,
   861  		},
   862  		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true},
   863  		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
   864  		PublicAccessPrevention:   PublicAccessPreventionEnforced,
   865  		VersioningEnabled:        false,
   866  		RPO:                      RPOAsyncTurbo,
   867  		Created:                  time.Now(),
   868  		Labels:                   map[string]string{"label": "value"},
   869  		CORS: []CORS{
   870  			{
   871  				MaxAge:          time.Hour,
   872  				Methods:         []string{"GET", "POST"},
   873  				Origins:         []string{"*"},
   874  				ResponseHeaders: []string{"FOO"},
   875  			},
   876  		},
   877  		Encryption:       &BucketEncryption{DefaultKMSKeyName: "key"},
   878  		Logging:          &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
   879  		Website:          &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
   880  		Autoclass:        &Autoclass{Enabled: true, TerminalStorageClass: "ARCHIVE"},
   881  		SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: time.Hour * 2},
   882  		Lifecycle: Lifecycle{
   883  			Rules: []LifecycleRule{{
   884  				Action: LifecycleAction{
   885  					Type: DeleteAction,
   886  				},
   887  				Condition: LifecycleCondition{
   888  					AgeInDays: 10,
   889  				},
   890  			}},
   891  		},
   892  		// Below fields should be ignored.
   893  		MetaGeneration: 39,
   894  		Etag:           "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
   895  	}
   896  	got := attrs.toProtoBucket()
   897  	autoclassTSC := "ARCHIVE"
   898  	want := &storagepb.Bucket{
   899  		Name: "name",
   900  		Acl: []*storagepb.BucketAccessControl{
   901  			{Entity: "bob@example.com", Role: "OWNER"},
   902  		},
   903  		DefaultObjectAcl: []*storagepb.ObjectAccessControl{
   904  			{Entity: "allUsers", Role: "READER"},
   905  		},
   906  		Location:     "loc",
   907  		StorageClass: "class",
   908  		RetentionPolicy: &storagepb.Bucket_RetentionPolicy{
   909  			RetentionDuration: durationpb.New(3 * time.Second),
   910  		},
   911  		IamConfig: &storagepb.Bucket_IamConfig{
   912  			UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
   913  				Enabled: true,
   914  			},
   915  			PublicAccessPrevention: "enforced",
   916  		},
   917  		Versioning: nil, // ignore VersioningEnabled if false
   918  		Rpo:        rpoAsyncTurbo,
   919  		Labels:     map[string]string{"label": "value"},
   920  		Cors: []*storagepb.Bucket_Cors{
   921  			{
   922  				MaxAgeSeconds:  3600,
   923  				Method:         []string{"GET", "POST"},
   924  				Origin:         []string{"*"},
   925  				ResponseHeader: []string{"FOO"},
   926  			},
   927  		},
   928  		Encryption:       &storagepb.Bucket_Encryption{DefaultKmsKey: "key"},
   929  		Logging:          &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"},
   930  		Website:          &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"},
   931  		Autoclass:        &storagepb.Bucket_Autoclass{Enabled: true, TerminalStorageClass: &autoclassTSC},
   932  		SoftDeletePolicy: &storagepb.Bucket_SoftDeletePolicy{RetentionDuration: durationpb.New(2 * time.Hour)},
   933  		Lifecycle: &storagepb.Bucket_Lifecycle{
   934  			Rule: []*storagepb.Bucket_Lifecycle_Rule{
   935  				{
   936  					Action: &storagepb.Bucket_Lifecycle_Rule_Action{Type: "Delete"},
   937  					Condition: &storagepb.Bucket_Lifecycle_Rule_Condition{
   938  						AgeDays:                 proto.Int32(int32(10)),
   939  						NumNewerVersions:        proto.Int32(int32(0)),
   940  						DaysSinceCustomTime:     proto.Int32(int32(0)),
   941  						DaysSinceNoncurrentTime: proto.Int32(int32(0)),
   942  					},
   943  				},
   944  			},
   945  		},
   946  	}
   947  
   948  	if msg := testutil.Diff(got, want); msg != "" {
   949  		t.Error(msg)
   950  	}
   951  
   952  	attrs.VersioningEnabled = true
   953  	attrs.RequesterPays = true
   954  	got = attrs.toProtoBucket()
   955  	want.Versioning = &storagepb.Bucket_Versioning{Enabled: true}
   956  	want.Billing = &storagepb.Bucket_Billing{RequesterPays: true}
   957  	if msg := testutil.Diff(got, want); msg != "" {
   958  		t.Error(msg)
   959  	}
   960  
   961  	// Test that setting either of BucketPolicyOnly or UniformBucketLevelAccess
   962  	// will enable UniformBucketLevelAccess.
   963  	// Set UBLA.Enabled = true --> UBLA should be set to enabled in the proto.
   964  	attrs.BucketPolicyOnly = BucketPolicyOnly{}
   965  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
   966  	got = attrs.toProtoBucket()
   967  	want.IamConfig = &storagepb.Bucket_IamConfig{
   968  		UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
   969  			Enabled: true,
   970  		},
   971  		PublicAccessPrevention: "enforced",
   972  	}
   973  	if msg := testutil.Diff(got, want); msg != "" {
   974  		t.Errorf(msg)
   975  	}
   976  
   977  	// Set BucketPolicyOnly.Enabled = true --> UBLA should be set to enabled in
   978  	// the proto.
   979  	attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true}
   980  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{}
   981  	got = attrs.toProtoBucket()
   982  	want.IamConfig = &storagepb.Bucket_IamConfig{
   983  		UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
   984  			Enabled: true,
   985  		},
   986  		PublicAccessPrevention: "enforced",
   987  	}
   988  	if msg := testutil.Diff(got, want); msg != "" {
   989  		t.Errorf(msg)
   990  	}
   991  
   992  	// Set both BucketPolicyOnly.Enabled = true and
   993  	// UniformBucketLevelAccess.Enabled=true --> UBLA should be set to enabled
   994  	// in the proto.
   995  	attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true}
   996  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
   997  	got = attrs.toProtoBucket()
   998  	want.IamConfig = &storagepb.Bucket_IamConfig{
   999  		UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
  1000  			Enabled: true,
  1001  		},
  1002  		PublicAccessPrevention: "enforced",
  1003  	}
  1004  	if msg := testutil.Diff(got, want); msg != "" {
  1005  		t.Errorf(msg)
  1006  	}
  1007  
  1008  	// Set UBLA.Enabled=false and BucketPolicyOnly.Enabled=false --> UBLA
  1009  	// should be disabled in the proto.
  1010  	attrs.BucketPolicyOnly = BucketPolicyOnly{}
  1011  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{}
  1012  	got = attrs.toProtoBucket()
  1013  	want.IamConfig = &storagepb.Bucket_IamConfig{
  1014  		PublicAccessPrevention: "enforced",
  1015  	}
  1016  	if msg := testutil.Diff(got, want); msg != "" {
  1017  		t.Errorf(msg)
  1018  	}
  1019  
  1020  	// Test that setting PublicAccessPrevention to "unspecified" leads to the
  1021  	// inherited setting being propagated in the proto.
  1022  	attrs.PublicAccessPrevention = PublicAccessPreventionUnspecified
  1023  	got = attrs.toProtoBucket()
  1024  	want.IamConfig = &storagepb.Bucket_IamConfig{
  1025  		PublicAccessPrevention: "inherited",
  1026  	}
  1027  	if msg := testutil.Diff(got, want); msg != "" {
  1028  		t.Errorf(msg)
  1029  	}
  1030  
  1031  	// Test that setting PublicAccessPrevention to "inherited" leads to the
  1032  	// setting being propagated in the proto.
  1033  	attrs.PublicAccessPrevention = PublicAccessPreventionInherited
  1034  	got = attrs.toProtoBucket()
  1035  	want.IamConfig = &storagepb.Bucket_IamConfig{
  1036  		PublicAccessPrevention: "inherited",
  1037  	}
  1038  	if msg := testutil.Diff(got, want); msg != "" {
  1039  		t.Errorf(msg)
  1040  	}
  1041  
  1042  	// Test that setting RPO to default is propagated in the proto.
  1043  	attrs.RPO = RPODefault
  1044  	got = attrs.toProtoBucket()
  1045  	want.Rpo = rpoDefault
  1046  	if msg := testutil.Diff(got, want); msg != "" {
  1047  		t.Errorf(msg)
  1048  	}
  1049  
  1050  	// Re-enable UBLA and confirm that it does not affect the PAP setting.
  1051  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
  1052  	got = attrs.toProtoBucket()
  1053  	want.IamConfig = &storagepb.Bucket_IamConfig{
  1054  		UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
  1055  			Enabled: true,
  1056  		},
  1057  		PublicAccessPrevention: "inherited",
  1058  	}
  1059  	if msg := testutil.Diff(got, want); msg != "" {
  1060  		t.Errorf(msg)
  1061  	}
  1062  
  1063  	// Disable UBLA and reset PAP to default. Confirm that the IAM config is set
  1064  	// to nil in the proto.
  1065  	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: false}
  1066  	attrs.PublicAccessPrevention = PublicAccessPreventionUnknown
  1067  	got = attrs.toProtoBucket()
  1068  	want.IamConfig = nil
  1069  	if msg := testutil.Diff(got, want); msg != "" {
  1070  		t.Errorf(msg)
  1071  	}
  1072  }
  1073  
  1074  func TestBucketRetryer(t *testing.T) {
  1075  	testCases := []struct {
  1076  		name string
  1077  		call func(b *BucketHandle) *BucketHandle
  1078  		want *retryConfig
  1079  	}{
  1080  		{
  1081  			name: "all defaults",
  1082  			call: func(b *BucketHandle) *BucketHandle {
  1083  				return b.Retryer()
  1084  			},
  1085  			want: &retryConfig{},
  1086  		},
  1087  		{
  1088  			name: "set all options",
  1089  			call: func(b *BucketHandle) *BucketHandle {
  1090  				return b.Retryer(
  1091  					WithBackoff(gax.Backoff{
  1092  						Initial:    2 * time.Second,
  1093  						Max:        30 * time.Second,
  1094  						Multiplier: 3,
  1095  					}),
  1096  					WithPolicy(RetryAlways),
  1097  					WithMaxAttempts(5),
  1098  					WithErrorFunc(func(err error) bool { return false }))
  1099  			},
  1100  			want: &retryConfig{
  1101  				backoff: &gax.Backoff{
  1102  					Initial:    2 * time.Second,
  1103  					Max:        30 * time.Second,
  1104  					Multiplier: 3,
  1105  				},
  1106  				policy:      RetryAlways,
  1107  				maxAttempts: expectedAttempts(5),
  1108  				shouldRetry: func(err error) bool { return false },
  1109  			},
  1110  		},
  1111  		{
  1112  			name: "set some backoff options",
  1113  			call: func(b *BucketHandle) *BucketHandle {
  1114  				return b.Retryer(
  1115  					WithBackoff(gax.Backoff{
  1116  						Multiplier: 3,
  1117  					}))
  1118  			},
  1119  			want: &retryConfig{
  1120  				backoff: &gax.Backoff{
  1121  					Multiplier: 3,
  1122  				}},
  1123  		},
  1124  		{
  1125  			name: "set policy only",
  1126  			call: func(b *BucketHandle) *BucketHandle {
  1127  				return b.Retryer(WithPolicy(RetryNever))
  1128  			},
  1129  			want: &retryConfig{
  1130  				policy: RetryNever,
  1131  			},
  1132  		},
  1133  		{
  1134  			name: "set max retry attempts only",
  1135  			call: func(b *BucketHandle) *BucketHandle {
  1136  				return b.Retryer(WithMaxAttempts(5))
  1137  			},
  1138  			want: &retryConfig{
  1139  				maxAttempts: expectedAttempts(5),
  1140  			},
  1141  		},
  1142  		{
  1143  			name: "set ErrorFunc only",
  1144  			call: func(b *BucketHandle) *BucketHandle {
  1145  				return b.Retryer(
  1146  					WithErrorFunc(func(err error) bool { return false }))
  1147  			},
  1148  			want: &retryConfig{
  1149  				shouldRetry: func(err error) bool { return false },
  1150  			},
  1151  		},
  1152  	}
  1153  	for _, tc := range testCases {
  1154  		t.Run(tc.name, func(s *testing.T) {
  1155  			b := tc.call(&BucketHandle{})
  1156  			if diff := cmp.Diff(
  1157  				b.retry,
  1158  				tc.want,
  1159  				cmp.AllowUnexported(retryConfig{}, gax.Backoff{}),
  1160  				// ErrorFunc cannot be compared directly, but we check if both are
  1161  				// either nil or non-nil.
  1162  				cmp.Comparer(func(a, b func(err error) bool) bool {
  1163  					return (a == nil && b == nil) || (a != nil && b != nil)
  1164  				}),
  1165  			); diff != "" {
  1166  				s.Fatalf("retry not configured correctly: %v", diff)
  1167  			}
  1168  		})
  1169  	}
  1170  }
  1171  
  1172  func TestDetectDefaultGoogleAccessID(t *testing.T) {
  1173  	testCases := []struct {
  1174  		name           string
  1175  		serviceAccount string
  1176  		creds          func(string) string
  1177  		expectSuccess  bool
  1178  	}{
  1179  		{
  1180  			name:           "impersonated creds",
  1181  			serviceAccount: "default@my-project.iam.gserviceaccount.com",
  1182  			creds: func(sa string) string {
  1183  				return fmt.Sprintf(`{
  1184  					"delegates": [],
  1185  					"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
  1186  					"source_credentials": {
  1187  					  "client_id": "id",
  1188  					  "client_secret": "secret",
  1189  					  "refresh_token": "token",
  1190  					  "type": "authorized_user"
  1191  					},
  1192  					"type": "impersonated_service_account"
  1193  				  }`, sa)
  1194  			},
  1195  			expectSuccess: true,
  1196  		},
  1197  		{
  1198  			name:           "gcloud ADC creds",
  1199  			serviceAccount: "default@my-project.iam.gserviceaccount.com",
  1200  			creds: func(sa string) string {
  1201  				return fmt.Sprint(`{
  1202  					"client_id": "my-id.apps.googleusercontent.com",
  1203  					"client_secret": "secret",
  1204  					"quota_project_id": "",
  1205  					"refresh_token": "token",
  1206  					"type": "authorized_user"
  1207  				}`)
  1208  			},
  1209  			expectSuccess: false,
  1210  		},
  1211  		{
  1212  			name:           "ADC private key",
  1213  			serviceAccount: "default@my-project.iam.gserviceaccount.com",
  1214  			creds: func(sa string) string {
  1215  				return fmt.Sprintf(`{
  1216  					"type": "service_account",
  1217  					"project_id": "my-project",
  1218  					"private_key_id": "my1",
  1219  					"private_key": "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n",
  1220  					"client_email": "%s",
  1221  					"client_id": "01",
  1222  					"auth_uri": "https://accounts.google.com/o/oauth2/auth",
  1223  					"token_uri": "https://oauth2.googleapis.com/token",
  1224  					"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  1225  					"client_x509_cert_url": "cert"
  1226  				}`, sa)
  1227  			},
  1228  			expectSuccess: true,
  1229  		},
  1230  		{
  1231  			name: "no creds",
  1232  			creds: func(_ string) string {
  1233  				return ""
  1234  			},
  1235  			expectSuccess: false,
  1236  		},
  1237  		{
  1238  			name:           "malformed creds",
  1239  			serviceAccount: "default@my-project.iam.gserviceaccount.com",
  1240  			creds: func(sa string) string {
  1241  				return fmt.Sprintf(`{
  1242  					"type": "service_account"
  1243  					"project_id": "my-project",
  1244  					"private_key_id": "my1",
  1245  					"private_key": "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n",
  1246  					"client_email": "%s",
  1247  				}`, sa)
  1248  			},
  1249  			expectSuccess: false,
  1250  		},
  1251  		{
  1252  			name:           "external creds",
  1253  			serviceAccount: "default@my-project.iam.gserviceaccount.com",
  1254  			creds: func(sa string) string {
  1255  				return fmt.Sprintf(`{
  1256  					"type": "external_account",
  1257  					"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
  1258  					"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
  1259  					"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
  1260  					"token_url": "https://sts.googleapis.com/v1/token",
  1261  					"credential_source": {
  1262  					  "environment_id": "id",
  1263  					  "region_url": "region_url",
  1264  					  "url": "url",
  1265  					  "regional_cred_verification_url": "ver_url",
  1266  					  "imdsv2_session_token_url": "tok_url"
  1267  					}
  1268  				  }`, sa)
  1269  			},
  1270  			expectSuccess: true,
  1271  		},
  1272  	}
  1273  
  1274  	for _, tc := range testCases {
  1275  		t.Run(tc.name, func(t *testing.T) {
  1276  			bucket := BucketHandle{
  1277  				c: &Client{
  1278  					creds: &google.Credentials{
  1279  						JSON: []byte(tc.creds(tc.serviceAccount)),
  1280  					},
  1281  				},
  1282  				name: "my-bucket",
  1283  			}
  1284  
  1285  			id, err := bucket.detectDefaultGoogleAccessID()
  1286  			if tc.expectSuccess {
  1287  				if err != nil {
  1288  					t.Fatal(err)
  1289  				}
  1290  				if id != tc.serviceAccount {
  1291  					t.Errorf("service account not found correctly; got: %s, want: %s", id, tc.serviceAccount)
  1292  				}
  1293  			} else if err == nil {
  1294  				t.Error("expected error but detectDefaultGoogleAccessID did not return one")
  1295  			}
  1296  		})
  1297  	}
  1298  }
  1299  
  1300  // TestBucketSignedURL_Endpoint_Emulator_Host tests that Bucket.SignedURl
  1301  // respects the host set in STORAGE_EMULATOR_HOST and/or in option.WithEndpoint
  1302  // TODO: move this testing to conformance tests.
  1303  func TestBucketSignedURL_Endpoint_Emulator_Host(t *testing.T) {
  1304  	expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
  1305  	bucketName := "bucket-name"
  1306  	objectName := "obj-name"
  1307  
  1308  	localhost9000 := "localhost:9000"
  1309  	localhost6000Https := "https://localhost:6000"
  1310  
  1311  	tests := []struct {
  1312  		desc         string
  1313  		emulatorHost string
  1314  		endpoint     *string
  1315  		now          time.Time
  1316  		opts         *SignedURLOptions
  1317  		// Note for future implementors: X-Goog-Signature generated by having
  1318  		// the client run through its algorithm with pre-defined input and copy
  1319  		// pasting the output. These tests are not great for testing whether
  1320  		// the right signature is calculated - instead we rely on the backend
  1321  		// and integration tests for that.
  1322  		want string
  1323  	}{
  1324  		{
  1325  			desc:         "SignURLV4 creates link to resources in emulator",
  1326  			emulatorHost: localhost9000,
  1327  			now:          expires.Add(-24 * time.Hour),
  1328  			opts: &SignedURLOptions{
  1329  				GoogleAccessID: "xxx@clientid",
  1330  				PrivateKey:     dummyKey("rsa"),
  1331  				Method:         "POST",
  1332  				Expires:        expires,
  1333  				Scheme:         SigningSchemeV4,
  1334  				Insecure:       true,
  1335  			},
  1336  			want: "http://localhost:9000/" + bucketName + "/" + objectName +
  1337  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1338  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1339  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1340  				"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
  1341  				"&X-Goog-SignedHeaders=host",
  1342  		},
  1343  		{
  1344  			desc:     "SignURLV4 - endpoint",
  1345  			endpoint: &localhost9000,
  1346  			now:      expires.Add(-24 * time.Hour),
  1347  			opts: &SignedURLOptions{
  1348  				GoogleAccessID: "xxx@clientid",
  1349  				PrivateKey:     dummyKey("rsa"),
  1350  				Method:         "POST",
  1351  				Expires:        expires,
  1352  				Scheme:         SigningSchemeV4,
  1353  				Insecure:       true,
  1354  			},
  1355  			want: "http://localhost:9000/" + bucketName + "/" + objectName +
  1356  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1357  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1358  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1359  				"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
  1360  				"&X-Goog-SignedHeaders=host",
  1361  		},
  1362  		{
  1363  			desc:         "SignURLV4 - endpoint takes precedence over emulator",
  1364  			endpoint:     &localhost9000,
  1365  			emulatorHost: "localhost:8000",
  1366  			now:          expires.Add(-24 * time.Hour),
  1367  			opts: &SignedURLOptions{
  1368  				GoogleAccessID: "xxx@clientid",
  1369  				PrivateKey:     dummyKey("rsa"),
  1370  				Method:         "POST",
  1371  				Expires:        expires,
  1372  				Scheme:         SigningSchemeV4,
  1373  				Insecure:       true,
  1374  			},
  1375  			want: "http://localhost:9000/" + bucketName + "/" + objectName +
  1376  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1377  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1378  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1379  				"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
  1380  				"&X-Goog-SignedHeaders=host",
  1381  		},
  1382  		{
  1383  			desc:         "SigningSchemeV2 - emulator",
  1384  			emulatorHost: "localhost:8000",
  1385  			now:          expires.Add(-24 * time.Hour),
  1386  			opts: &SignedURLOptions{
  1387  				GoogleAccessID: "xxx@clientid",
  1388  				PrivateKey:     dummyKey("rsa"),
  1389  				Method:         "POST",
  1390  				Expires:        expires,
  1391  				Scheme:         SigningSchemeV2,
  1392  			},
  1393  			want: "https://localhost:8000/" + bucketName + "/" + objectName +
  1394  				"?Expires=1033570800" +
  1395  				"&GoogleAccessId=xxx%40clientid" +
  1396  				"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
  1397  		},
  1398  		{
  1399  			desc:         "SigningSchemeV2 - endpoint",
  1400  			emulatorHost: "localhost:8000",
  1401  			endpoint:     &localhost9000,
  1402  			now:          expires.Add(-24 * time.Hour),
  1403  			opts: &SignedURLOptions{
  1404  				GoogleAccessID: "xxx@clientid",
  1405  				PrivateKey:     dummyKey("rsa"),
  1406  				Method:         "POST",
  1407  				Expires:        expires,
  1408  				Scheme:         SigningSchemeV2,
  1409  			},
  1410  			want: "https://localhost:9000/" + bucketName + "/" + objectName +
  1411  				"?Expires=1033570800" +
  1412  				"&GoogleAccessId=xxx%40clientid" +
  1413  				"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
  1414  		},
  1415  		{
  1416  			desc:         "VirtualHostedStyle - emulator",
  1417  			emulatorHost: "localhost:8000",
  1418  			now:          expires.Add(-24 * time.Hour),
  1419  			opts: &SignedURLOptions{
  1420  				GoogleAccessID: "xxx@clientid",
  1421  				PrivateKey:     dummyKey("rsa"),
  1422  				Method:         "POST",
  1423  				Expires:        expires,
  1424  				Scheme:         SigningSchemeV4,
  1425  				Style:          VirtualHostedStyle(),
  1426  				Insecure:       true,
  1427  			},
  1428  			want: "http://" + bucketName + ".localhost:8000/" + objectName +
  1429  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1430  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1431  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1432  				"&X-Goog-Signature=35e0b9d33901a2518956821175f88c2c4eb3f4461b725af74b37c36d23f8bbe927558ac57b0be40d345f20bca55ba0652d38b7a620f8da68d4f733706ad104da468c3a039459acf35f3022e388760cd49893c998c33fe3ccc8c022d7034ab98bdbdcac4b680bb24ae5ed586a42ee9495a873ffc484e297853a8a3892d0d6385c980cb7e3c5c8bdd4939b4c17105f10fe8b5b9744017bf59431ff176c1550ae1c64ddd6628096eb6895c97c5da4d850aca72c14b7f5018c15b34d4b00ec63ff2ccb688ddbef2d32648e247ffd0137498080f320f293eb811a94fb526227324bbbd01335446388797803e67d802f97b52565deba3d2387ecabf4f3094662236017" +
  1433  				"&X-Goog-SignedHeaders=host",
  1434  		},
  1435  		{
  1436  			desc:         "VirtualHostedStyle - endpoint overrides emulator",
  1437  			emulatorHost: "localhost:8000",
  1438  			endpoint:     &localhost9000,
  1439  			now:          expires.Add(-24 * time.Hour),
  1440  			opts: &SignedURLOptions{
  1441  				GoogleAccessID: "xxx@clientid",
  1442  				PrivateKey:     dummyKey("rsa"),
  1443  				Method:         "POST",
  1444  				Expires:        expires,
  1445  				Scheme:         SigningSchemeV4,
  1446  				Style:          VirtualHostedStyle(),
  1447  				Insecure:       true,
  1448  			},
  1449  			want: "http://" + bucketName + ".localhost:9000/" + objectName +
  1450  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1451  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1452  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1453  				"&X-Goog-Signature=35e0b9d33901a2518956821175f88c2c4eb3f4461b725af74b37c36d23f8bbe927558ac57b0be40d345f20bca55ba0652d38b7a620f8da68d4f733706ad104da468c3a039459acf35f3022e388760cd49893c998c33fe3ccc8c022d7034ab98bdbdcac4b680bb24ae5ed586a42ee9495a873ffc484e297853a8a3892d0d6385c980cb7e3c5c8bdd4939b4c17105f10fe8b5b9744017bf59431ff176c1550ae1c64ddd6628096eb6895c97c5da4d850aca72c14b7f5018c15b34d4b00ec63ff2ccb688ddbef2d32648e247ffd0137498080f320f293eb811a94fb526227324bbbd01335446388797803e67d802f97b52565deba3d2387ecabf4f3094662236017" +
  1454  				"&X-Goog-SignedHeaders=host",
  1455  		},
  1456  		{
  1457  			desc:         "BucketBoundHostname - emulator",
  1458  			emulatorHost: "localhost:8000",
  1459  			now:          expires.Add(-24 * time.Hour),
  1460  			opts: &SignedURLOptions{
  1461  				GoogleAccessID: "xxx@clientid",
  1462  				PrivateKey:     dummyKey("rsa"),
  1463  				Method:         "POST",
  1464  				Expires:        expires,
  1465  				Scheme:         SigningSchemeV4,
  1466  				Style:          BucketBoundHostname("myhost"),
  1467  			},
  1468  			want: "https://" + "myhost/" + objectName +
  1469  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1470  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1471  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1472  				"&X-Goog-Signature=15fe19f6c61bcbdbd6473c32f2bec29caa8a5fa3b2ce32cfb5329a71edaa0d4e5ffe6469f32ed4c23ca2fbed3882fdf1ed107c6a98c2c4995dda6036c64bae51e6cb542c353618f483832aa1f3ef85342ddadd69c13ad4c69fd3f573ea5cf325a58056e3d5a37005217662af63b49fef8688de3c5c7a2f7b43651a030edd0813eb7f7713989a4c29a8add65133ce652895fea9de7dbc6248ee11b4d7c6c1e152df87700100e896e544ba8eeea96584078f56e699665140b750e90550b9b79633f4e7c8409efa807be5670d6e987eeee04a4180be9b9e30bb8557597beaf390a3805cc602c87a3e34800f8bc01449c3dd10ac2f2263e55e55b91e445052548d5e" +
  1473  				"&X-Goog-SignedHeaders=host",
  1474  		},
  1475  		{
  1476  			desc:     "BucketBoundHostname - endpoint",
  1477  			endpoint: &localhost9000,
  1478  			now:      expires.Add(-24 * time.Hour),
  1479  			opts: &SignedURLOptions{
  1480  				GoogleAccessID: "xxx@clientid",
  1481  				PrivateKey:     dummyKey("rsa"),
  1482  				Method:         "POST",
  1483  				Expires:        expires,
  1484  				Scheme:         SigningSchemeV4,
  1485  				Style:          BucketBoundHostname("myhost"),
  1486  			},
  1487  			want: "https://" + "myhost/" + objectName +
  1488  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1489  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1490  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1491  				"&X-Goog-Signature=15fe19f6c61bcbdbd6473c32f2bec29caa8a5fa3b2ce32cfb5329a71edaa0d4e5ffe6469f32ed4c23ca2fbed3882fdf1ed107c6a98c2c4995dda6036c64bae51e6cb542c353618f483832aa1f3ef85342ddadd69c13ad4c69fd3f573ea5cf325a58056e3d5a37005217662af63b49fef8688de3c5c7a2f7b43651a030edd0813eb7f7713989a4c29a8add65133ce652895fea9de7dbc6248ee11b4d7c6c1e152df87700100e896e544ba8eeea96584078f56e699665140b750e90550b9b79633f4e7c8409efa807be5670d6e987eeee04a4180be9b9e30bb8557597beaf390a3805cc602c87a3e34800f8bc01449c3dd10ac2f2263e55e55b91e445052548d5e" +
  1492  				"&X-Goog-SignedHeaders=host",
  1493  		},
  1494  		{
  1495  			desc:         "emulator host specifies scheme",
  1496  			emulatorHost: "https://localhost:6000",
  1497  			now:          expires.Add(-24 * time.Hour),
  1498  			opts: &SignedURLOptions{
  1499  				GoogleAccessID: "xxx@clientid",
  1500  				PrivateKey:     dummyKey("rsa"),
  1501  				Method:         "POST",
  1502  				Expires:        expires,
  1503  				Scheme:         SigningSchemeV4, //do v2 here
  1504  				Insecure:       true,
  1505  			},
  1506  			want: "http://localhost:6000/" + bucketName + "/" + objectName +
  1507  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1508  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1509  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1510  				"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
  1511  				"&X-Goog-SignedHeaders=host",
  1512  		},
  1513  		{
  1514  			desc:     "endpoint specifies scheme",
  1515  			endpoint: &localhost6000Https,
  1516  			now:      expires.Add(-24 * time.Hour),
  1517  			opts: &SignedURLOptions{
  1518  				GoogleAccessID: "xxx@clientid",
  1519  				PrivateKey:     dummyKey("rsa"),
  1520  				Method:         "POST",
  1521  				Expires:        expires,
  1522  				Scheme:         SigningSchemeV4,
  1523  				Insecure:       true,
  1524  			},
  1525  			want: "http://localhost:6000/" + bucketName + "/" + objectName +
  1526  				"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
  1527  				"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
  1528  				"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
  1529  				"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
  1530  				"&X-Goog-SignedHeaders=host",
  1531  		},
  1532  		{
  1533  			desc:         "emulator host specifies scheme using SigningSchemeV2",
  1534  			emulatorHost: "https://localhost:8000",
  1535  			now:          expires.Add(-24 * time.Hour),
  1536  			opts: &SignedURLOptions{
  1537  				GoogleAccessID: "xxx@clientid",
  1538  				PrivateKey:     dummyKey("rsa"),
  1539  				Method:         "POST",
  1540  				Expires:        expires,
  1541  				Scheme:         SigningSchemeV2,
  1542  			},
  1543  			want: "https://localhost:8000/" + bucketName + "/" + objectName +
  1544  				"?Expires=1033570800" +
  1545  				"&GoogleAccessId=xxx%40clientid" +
  1546  				"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
  1547  		},
  1548  		{
  1549  			desc:     "endpoint specifies scheme using SigningSchemeV2",
  1550  			endpoint: &localhost6000Https,
  1551  			now:      expires.Add(-24 * time.Hour),
  1552  			opts: &SignedURLOptions{
  1553  				GoogleAccessID: "xxx@clientid",
  1554  				PrivateKey:     dummyKey("rsa"),
  1555  				Method:         "POST",
  1556  				Expires:        expires,
  1557  				Scheme:         SigningSchemeV2,
  1558  			},
  1559  			want: "https://localhost:6000/" + bucketName + "/" + objectName +
  1560  				"?Expires=1033570800" +
  1561  				"&GoogleAccessId=xxx%40clientid" +
  1562  				"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
  1563  		},
  1564  		{
  1565  			desc:         "hostname in opts overrides all else",
  1566  			endpoint:     &localhost9000,
  1567  			emulatorHost: "https://localhost:8000",
  1568  			now:          expires.Add(-24 * time.Hour),
  1569  			opts: &SignedURLOptions{
  1570  				GoogleAccessID: "xxx@clientid",
  1571  				PrivateKey:     dummyKey("rsa"),
  1572  				Method:         "POST",
  1573  				Expires:        expires,
  1574  				Scheme:         SigningSchemeV2,
  1575  				Hostname:       "localhost:6000",
  1576  			},
  1577  			want: "https://localhost:6000/" + bucketName + "/" + objectName +
  1578  				"?Expires=1033570800" +
  1579  				"&GoogleAccessId=xxx%40clientid" +
  1580  				"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
  1581  		},
  1582  	}
  1583  	oldUTCNow := utcNow
  1584  	defer func() {
  1585  		utcNow = oldUTCNow
  1586  	}()
  1587  
  1588  	for _, test := range tests {
  1589  		t.Run(test.desc, func(s *testing.T) {
  1590  			utcNow = func() time.Time {
  1591  				return test.now
  1592  			}
  1593  
  1594  			t.Setenv("STORAGE_EMULATOR_HOST", test.emulatorHost)
  1595  
  1596  			var opts []option.ClientOption
  1597  			if test.endpoint != nil {
  1598  				opts = append(opts, option.WithEndpoint(*test.endpoint), option.WithoutAuthentication())
  1599  			}
  1600  			c, err := NewClient(context.Background(), opts...)
  1601  			if err != nil {
  1602  				t.Fatalf("NewClient: %v", err)
  1603  			}
  1604  
  1605  			got, err := c.Bucket(bucketName).SignedURL(objectName, test.opts)
  1606  			if err != nil {
  1607  				s.Fatal(err)
  1608  			}
  1609  
  1610  			if got != test.want {
  1611  				s.Fatalf("bucket.SidnedURL:\n\tgot:\t%v\n\twant:\t%v", got, test.want)
  1612  			}
  1613  		})
  1614  	}
  1615  }
  1616  

View as plain text