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
15package storage
16
17import (
18	"net/http"
19	"reflect"
20	"testing"
21	"time"
22
23	"cloud.google.com/go/internal/testutil"
24	"github.com/google/go-cmp/cmp"
25	"google.golang.org/api/googleapi"
26	raw "google.golang.org/api/storage/v1"
27)
28
29func TestBucketAttrsToRawBucket(t *testing.T) {
30	t.Parallel()
31	attrs := &BucketAttrs{
32		Name: "name",
33		ACL:  []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}},
34		DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid",
35			ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}},
36		Etag:         "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
37		Location:     "loc",
38		StorageClass: "class",
39		RetentionPolicy: &RetentionPolicy{
40			RetentionPeriod: 3 * time.Second,
41		},
42		BucketPolicyOnly:  BucketPolicyOnly{Enabled: true},
43		VersioningEnabled: false,
44		// should be ignored:
45		MetaGeneration: 39,
46		Created:        time.Now(),
47		Labels:         map[string]string{"label": "value"},
48		CORS: []CORS{
49			{
50				MaxAge:          time.Hour,
51				Methods:         []string{"GET", "POST"},
52				Origins:         []string{"*"},
53				ResponseHeaders: []string{"FOO"},
54			},
55		},
56		Encryption: &BucketEncryption{DefaultKMSKeyName: "key"},
57		Logging:    &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
58		Website:    &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
59		Lifecycle: Lifecycle{
60			Rules: []LifecycleRule{{
61				Action: LifecycleAction{
62					Type:         SetStorageClassAction,
63					StorageClass: "NEARLINE",
64				},
65				Condition: LifecycleCondition{
66					AgeInDays:             10,
67					Liveness:              Live,
68					CreatedBefore:         time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
69					MatchesStorageClasses: []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"},
70					NumNewerVersions:      3,
71				},
72			}, {
73				Action: LifecycleAction{
74					Type: DeleteAction,
75				},
76				Condition: LifecycleCondition{
77					AgeInDays:             30,
78					Liveness:              Live,
79					CreatedBefore:         time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
80					MatchesStorageClasses: []string{"NEARLINE"},
81					NumNewerVersions:      10,
82				},
83			}, {
84				Action: LifecycleAction{
85					Type: DeleteAction,
86				},
87				Condition: LifecycleCondition{
88					Liveness: Archived,
89				},
90			}},
91		},
92	}
93	got := attrs.toRawBucket()
94	want := &raw.Bucket{
95		Name: "name",
96		Acl: []*raw.BucketAccessControl{
97			{Entity: "bob@example.com", Role: "OWNER"}, // other fields ignored on create/update
98		},
99		DefaultObjectAcl: []*raw.ObjectAccessControl{
100			{Entity: "allUsers", Role: "READER"}, // other fields ignored on create/update
101		},
102		Location:     "loc",
103		StorageClass: "class",
104		RetentionPolicy: &raw.BucketRetentionPolicy{
105			RetentionPeriod: 3,
106		},
107		IamConfiguration: &raw.BucketIamConfiguration{
108			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
109				Enabled: true,
110			},
111		},
112		Versioning: nil, // ignore VersioningEnabled if false
113		Labels:     map[string]string{"label": "value"},
114		Cors: []*raw.BucketCors{
115			{
116				MaxAgeSeconds:  3600,
117				Method:         []string{"GET", "POST"},
118				Origin:         []string{"*"},
119				ResponseHeader: []string{"FOO"},
120			},
121		},
122		Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key"},
123		Logging:    &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
124		Website:    &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
125		Lifecycle: &raw.BucketLifecycle{
126			Rule: []*raw.BucketLifecycleRule{{
127				Action: &raw.BucketLifecycleRuleAction{
128					Type:         SetStorageClassAction,
129					StorageClass: "NEARLINE",
130				},
131				Condition: &raw.BucketLifecycleRuleCondition{
132					Age:                 10,
133					IsLive:              googleapi.Bool(true),
134					CreatedBefore:       "2017-01-02",
135					MatchesStorageClass: []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"},
136					NumNewerVersions:    3,
137				},
138			}, {
139				Action: &raw.BucketLifecycleRuleAction{
140					Type: DeleteAction,
141				},
142				Condition: &raw.BucketLifecycleRuleCondition{
143					Age:                 30,
144					IsLive:              googleapi.Bool(true),
145					CreatedBefore:       "2017-01-02",
146					MatchesStorageClass: []string{"NEARLINE"},
147					NumNewerVersions:    10,
148				},
149			}, {
150				Action: &raw.BucketLifecycleRuleAction{
151					Type: DeleteAction,
152				},
153				Condition: &raw.BucketLifecycleRuleCondition{
154					IsLive: googleapi.Bool(false),
155				},
156			}},
157		},
158	}
159	if msg := testutil.Diff(got, want); msg != "" {
160		t.Error(msg)
161	}
162
163	attrs.VersioningEnabled = true
164	attrs.RequesterPays = true
165	got = attrs.toRawBucket()
166	want.Versioning = &raw.BucketVersioning{Enabled: true}
167	want.Billing = &raw.BucketBilling{RequesterPays: true}
168	if msg := testutil.Diff(got, want); msg != "" {
169		t.Error(msg)
170	}
171}
172
173func TestBucketAttrsToUpdateToRawBucket(t *testing.T) {
174	t.Parallel()
175	au := &BucketAttrsToUpdate{
176		VersioningEnabled:     false,
177		RequesterPays:         false,
178		BucketPolicyOnly:      &BucketPolicyOnly{Enabled: false},
179		DefaultEventBasedHold: false,
180		RetentionPolicy:       &RetentionPolicy{RetentionPeriod: time.Hour},
181		Encryption:            &BucketEncryption{DefaultKMSKeyName: "key2"},
182		Lifecycle: &Lifecycle{
183			Rules: []LifecycleRule{
184				{
185					Action:    LifecycleAction{Type: "Delete"},
186					Condition: LifecycleCondition{AgeInDays: 30},
187				},
188			},
189		},
190		Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
191		Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
192	}
193	au.SetLabel("a", "foo")
194	au.DeleteLabel("b")
195	au.SetLabel("c", "")
196	got := au.toRawBucket()
197	want := &raw.Bucket{
198		Versioning: &raw.BucketVersioning{
199			Enabled:         false,
200			ForceSendFields: []string{"Enabled"},
201		},
202		Labels: map[string]string{
203			"a": "foo",
204			"c": "",
205		},
206		Billing: &raw.BucketBilling{
207			RequesterPays:   false,
208			ForceSendFields: []string{"RequesterPays"},
209		},
210		DefaultEventBasedHold: false,
211		RetentionPolicy:       &raw.BucketRetentionPolicy{RetentionPeriod: 3600},
212		IamConfiguration: &raw.BucketIamConfiguration{
213			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
214				Enabled: false,
215			},
216		},
217		Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key2"},
218		NullFields: []string{"Labels.b"},
219		Lifecycle: &raw.BucketLifecycle{
220			Rule: []*raw.BucketLifecycleRule{
221				{
222					Action:    &raw.BucketLifecycleRuleAction{Type: "Delete"},
223					Condition: &raw.BucketLifecycleRuleCondition{Age: 30},
224				},
225			},
226		},
227		Logging:         &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
228		Website:         &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
229		ForceSendFields: []string{"DefaultEventBasedHold"},
230	}
231	if msg := testutil.Diff(got, want); msg != "" {
232		t.Error(msg)
233	}
234
235	var au2 BucketAttrsToUpdate
236	au2.DeleteLabel("b")
237	got = au2.toRawBucket()
238	want = &raw.Bucket{
239		Labels:          map[string]string{},
240		ForceSendFields: []string{"Labels"},
241		NullFields:      []string{"Labels.b"},
242	}
243	if msg := testutil.Diff(got, want); msg != "" {
244		t.Error(msg)
245	}
246
247	// Test nulls.
248	au3 := &BucketAttrsToUpdate{
249		RetentionPolicy: &RetentionPolicy{},
250		Encryption:      &BucketEncryption{},
251		Logging:         &BucketLogging{},
252		Website:         &BucketWebsite{},
253	}
254	got = au3.toRawBucket()
255	want = &raw.Bucket{
256		NullFields: []string{"RetentionPolicy", "Encryption", "Logging", "Website"},
257	}
258	if msg := testutil.Diff(got, want); msg != "" {
259		t.Error(msg)
260	}
261}
262
263func TestCallBuilders(t *testing.T) {
264	rc, err := raw.New(&http.Client{})
265	if err != nil {
266		t.Fatal(err)
267	}
268	c := &Client{raw: rc}
269	const metagen = 17
270
271	b := c.Bucket("name")
272	bm := b.If(BucketConditions{MetagenerationMatch: metagen}).UserProject("p")
273
274	equal := func(x, y interface{}) bool {
275		return testutil.Equal(x, y,
276			cmp.AllowUnexported(
277				raw.BucketsGetCall{},
278				raw.BucketsDeleteCall{},
279				raw.BucketsPatchCall{},
280			),
281			cmp.FilterPath(func(p cmp.Path) bool {
282				return p[len(p)-1].Type() == reflect.TypeOf(&raw.Service{})
283			}, cmp.Ignore()),
284		)
285	}
286
287	for i, test := range []struct {
288		callFunc func(*BucketHandle) (interface{}, error)
289		want     interface {
290			Header() http.Header
291		}
292		metagenFunc func(interface{})
293	}{
294		{
295			func(b *BucketHandle) (interface{}, error) { return b.newGetCall() },
296			rc.Buckets.Get("name").Projection("full"),
297			func(req interface{}) { req.(*raw.BucketsGetCall).IfMetagenerationMatch(metagen).UserProject("p") },
298		},
299		{
300			func(b *BucketHandle) (interface{}, error) { return b.newDeleteCall() },
301			rc.Buckets.Delete("name"),
302			func(req interface{}) { req.(*raw.BucketsDeleteCall).IfMetagenerationMatch(metagen).UserProject("p") },
303		},
304		{
305			func(b *BucketHandle) (interface{}, error) {
306				return b.newPatchCall(&BucketAttrsToUpdate{
307					VersioningEnabled: false,
308					RequesterPays:     false,
309				})
310			},
311			rc.Buckets.Patch("name", &raw.Bucket{
312				Versioning: &raw.BucketVersioning{
313					Enabled:         false,
314					ForceSendFields: []string{"Enabled"},
315				},
316				Billing: &raw.BucketBilling{
317					RequesterPays:   false,
318					ForceSendFields: []string{"RequesterPays"},
319				},
320			}).Projection("full"),
321			func(req interface{}) { req.(*raw.BucketsPatchCall).IfMetagenerationMatch(metagen).UserProject("p") },
322		},
323	} {
324		got, err := test.callFunc(b)
325		if err != nil {
326			t.Fatal(err)
327		}
328		setClientHeader(test.want.Header())
329		if !equal(got, test.want) {
330			t.Errorf("#%d: got %#v, want %#v", i, got, test.want)
331		}
332		got, err = test.callFunc(bm)
333		if err != nil {
334			t.Fatal(err)
335		}
336		test.metagenFunc(test.want)
337		if !equal(got, test.want) {
338			t.Errorf("#%d:\ngot  %#v\nwant %#v", i, got, test.want)
339		}
340	}
341
342	// Error.
343	bm = b.If(BucketConditions{MetagenerationMatch: 1, MetagenerationNotMatch: 2})
344	if _, err := bm.newGetCall(); err == nil {
345		t.Errorf("got nil, want error")
346	}
347	if _, err := bm.newDeleteCall(); err == nil {
348		t.Errorf("got nil, want error")
349	}
350	if _, err := bm.newPatchCall(&BucketAttrsToUpdate{}); err == nil {
351		t.Errorf("got nil, want error")
352	}
353}
354
355func TestNewBucket(t *testing.T) {
356	labels := map[string]string{"a": "b"}
357	matchClasses := []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"}
358	aTime := time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC)
359	rb := &raw.Bucket{
360		Name:                  "name",
361		Location:              "loc",
362		DefaultEventBasedHold: true,
363		Metageneration:        3,
364		StorageClass:          "sc",
365		TimeCreated:           "2017-10-23T04:05:06Z",
366		Versioning:            &raw.BucketVersioning{Enabled: true},
367		Labels:                labels,
368		Billing:               &raw.BucketBilling{RequesterPays: true},
369		Etag:                  "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
370		Lifecycle: &raw.BucketLifecycle{
371			Rule: []*raw.BucketLifecycleRule{{
372				Action: &raw.BucketLifecycleRuleAction{
373					Type:         "SetStorageClass",
374					StorageClass: "NEARLINE",
375				},
376				Condition: &raw.BucketLifecycleRuleCondition{
377					Age:                 10,
378					IsLive:              googleapi.Bool(true),
379					CreatedBefore:       "2017-01-02",
380					MatchesStorageClass: matchClasses,
381					NumNewerVersions:    3,
382				},
383			}},
384		},
385		RetentionPolicy: &raw.BucketRetentionPolicy{
386			RetentionPeriod: 3,
387			EffectiveTime:   aTime.Format(time.RFC3339),
388		},
389		IamConfiguration: &raw.BucketIamConfiguration{
390			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
391				Enabled:    true,
392				LockedTime: aTime.Format(time.RFC3339),
393			},
394		},
395		Cors: []*raw.BucketCors{
396			{
397				MaxAgeSeconds:  3600,
398				Method:         []string{"GET", "POST"},
399				Origin:         []string{"*"},
400				ResponseHeader: []string{"FOO"},
401			},
402		},
403		Acl: []*raw.BucketAccessControl{
404			{Bucket: "name", Role: "READER", Email: "joe@example.com", Entity: "allUsers"},
405		},
406		Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key"},
407		Logging:    &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
408		Website:    &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
409	}
410	want := &BucketAttrs{
411		Name:                  "name",
412		Location:              "loc",
413		DefaultEventBasedHold: true,
414		MetaGeneration:        3,
415		StorageClass:          "sc",
416		Created:               time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
417		VersioningEnabled:     true,
418		Labels:                labels,
419		Etag:                  "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
420		RequesterPays:         true,
421		Lifecycle: Lifecycle{
422			Rules: []LifecycleRule{
423				{
424					Action: LifecycleAction{
425						Type:         SetStorageClassAction,
426						StorageClass: "NEARLINE",
427					},
428					Condition: LifecycleCondition{
429						AgeInDays:             10,
430						Liveness:              Live,
431						CreatedBefore:         time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC),
432						MatchesStorageClasses: matchClasses,
433						NumNewerVersions:      3,
434					},
435				},
436			},
437		},
438		RetentionPolicy: &RetentionPolicy{
439			EffectiveTime:   aTime,
440			RetentionPeriod: 3 * time.Second,
441		},
442		BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: aTime},
443		CORS: []CORS{
444			{
445				MaxAge:          time.Hour,
446				Methods:         []string{"GET", "POST"},
447				Origins:         []string{"*"},
448				ResponseHeaders: []string{"FOO"},
449			},
450		},
451		Encryption:       &BucketEncryption{DefaultKMSKeyName: "key"},
452		Logging:          &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
453		Website:          &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
454		ACL:              []ACLRule{{Entity: "allUsers", Role: RoleReader, Email: "joe@example.com"}},
455		DefaultObjectACL: nil,
456	}
457	got, err := newBucket(rb)
458	if err != nil {
459		t.Fatal(err)
460	}
461	if diff := testutil.Diff(got, want); diff != "" {
462		t.Errorf("got=-, want=+:\n%s", diff)
463	}
464}
465