1// Copyright 2015 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 bigquery
16
17import (
18	"errors"
19	"strconv"
20	"testing"
21	"time"
22
23	"github.com/google/go-cmp/cmp"
24
25	"cloud.google.com/go/internal/testutil"
26
27	"golang.org/x/net/context"
28	bq "google.golang.org/api/bigquery/v2"
29	itest "google.golang.org/api/iterator/testing"
30)
31
32// readServiceStub services read requests by returning data from an in-memory list of values.
33type listTablesStub struct {
34	expectedProject, expectedDataset string
35	tables                           []*bq.TableListTables
36}
37
38func (s *listTablesStub) listTables(it *TableIterator, pageSize int, pageToken string) (*bq.TableList, error) {
39	if it.dataset.ProjectID != s.expectedProject {
40		return nil, errors.New("wrong project id")
41	}
42	if it.dataset.DatasetID != s.expectedDataset {
43		return nil, errors.New("wrong dataset id")
44	}
45	const maxPageSize = 2
46	if pageSize <= 0 || pageSize > maxPageSize {
47		pageSize = maxPageSize
48	}
49	start := 0
50	if pageToken != "" {
51		var err error
52		start, err = strconv.Atoi(pageToken)
53		if err != nil {
54			return nil, err
55		}
56	}
57	end := start + pageSize
58	if end > len(s.tables) {
59		end = len(s.tables)
60	}
61	nextPageToken := ""
62	if end < len(s.tables) {
63		nextPageToken = strconv.Itoa(end)
64	}
65	return &bq.TableList{
66		Tables:        s.tables[start:end],
67		NextPageToken: nextPageToken,
68	}, nil
69}
70
71func TestTables(t *testing.T) {
72	c := &Client{projectID: "p1"}
73	inTables := []*bq.TableListTables{
74		{TableReference: &bq.TableReference{ProjectId: "p1", DatasetId: "d1", TableId: "t1"}},
75		{TableReference: &bq.TableReference{ProjectId: "p1", DatasetId: "d1", TableId: "t2"}},
76		{TableReference: &bq.TableReference{ProjectId: "p1", DatasetId: "d1", TableId: "t3"}},
77	}
78	outTables := []*Table{
79		{ProjectID: "p1", DatasetID: "d1", TableID: "t1", c: c},
80		{ProjectID: "p1", DatasetID: "d1", TableID: "t2", c: c},
81		{ProjectID: "p1", DatasetID: "d1", TableID: "t3", c: c},
82	}
83
84	lts := &listTablesStub{
85		expectedProject: "p1",
86		expectedDataset: "d1",
87		tables:          inTables,
88	}
89	old := listTables
90	listTables = lts.listTables // cannot use t.Parallel with this test
91	defer func() { listTables = old }()
92
93	msg, ok := itest.TestIterator(outTables,
94		func() interface{} { return c.Dataset("d1").Tables(context.Background()) },
95		func(it interface{}) (interface{}, error) { return it.(*TableIterator).Next() })
96	if !ok {
97		t.Error(msg)
98	}
99}
100
101type listDatasetsStub struct {
102	expectedProject string
103	datasets        []*bq.DatasetListDatasets
104	hidden          map[*bq.DatasetListDatasets]bool
105}
106
107func (s *listDatasetsStub) listDatasets(it *DatasetIterator, pageSize int, pageToken string) (*bq.DatasetList, error) {
108	const maxPageSize = 2
109	if pageSize <= 0 || pageSize > maxPageSize {
110		pageSize = maxPageSize
111	}
112	if it.Filter != "" {
113		return nil, errors.New("filter not supported")
114	}
115	if it.ProjectID != s.expectedProject {
116		return nil, errors.New("bad project ID")
117	}
118	start := 0
119	if pageToken != "" {
120		var err error
121		start, err = strconv.Atoi(pageToken)
122		if err != nil {
123			return nil, err
124		}
125	}
126	var (
127		i             int
128		result        []*bq.DatasetListDatasets
129		nextPageToken string
130	)
131	for i = start; len(result) < pageSize && i < len(s.datasets); i++ {
132		if s.hidden[s.datasets[i]] && !it.ListHidden {
133			continue
134		}
135		result = append(result, s.datasets[i])
136	}
137	if i < len(s.datasets) {
138		nextPageToken = strconv.Itoa(i)
139	}
140	return &bq.DatasetList{
141		Datasets:      result,
142		NextPageToken: nextPageToken,
143	}, nil
144}
145
146func TestDatasets(t *testing.T) {
147	client := &Client{projectID: "p"}
148	inDatasets := []*bq.DatasetListDatasets{
149		{DatasetReference: &bq.DatasetReference{ProjectId: "p", DatasetId: "a"}},
150		{DatasetReference: &bq.DatasetReference{ProjectId: "p", DatasetId: "b"}},
151		{DatasetReference: &bq.DatasetReference{ProjectId: "p", DatasetId: "hidden"}},
152		{DatasetReference: &bq.DatasetReference{ProjectId: "p", DatasetId: "c"}},
153	}
154	outDatasets := []*Dataset{
155		{"p", "a", client},
156		{"p", "b", client},
157		{"p", "hidden", client},
158		{"p", "c", client},
159	}
160	lds := &listDatasetsStub{
161		expectedProject: "p",
162		datasets:        inDatasets,
163		hidden:          map[*bq.DatasetListDatasets]bool{inDatasets[2]: true},
164	}
165	old := listDatasets
166	listDatasets = lds.listDatasets // cannot use t.Parallel with this test
167	defer func() { listDatasets = old }()
168
169	msg, ok := itest.TestIterator(outDatasets,
170		func() interface{} { it := client.Datasets(context.Background()); it.ListHidden = true; return it },
171		func(it interface{}) (interface{}, error) { return it.(*DatasetIterator).Next() })
172	if !ok {
173		t.Fatalf("ListHidden=true: %s", msg)
174	}
175
176	msg, ok = itest.TestIterator([]*Dataset{outDatasets[0], outDatasets[1], outDatasets[3]},
177		func() interface{} { it := client.Datasets(context.Background()); it.ListHidden = false; return it },
178		func(it interface{}) (interface{}, error) { return it.(*DatasetIterator).Next() })
179	if !ok {
180		t.Fatalf("ListHidden=false: %s", msg)
181	}
182}
183
184func TestDatasetToBQ(t *testing.T) {
185	for _, test := range []struct {
186		in   *DatasetMetadata
187		want *bq.Dataset
188	}{
189		{nil, &bq.Dataset{}},
190		{&DatasetMetadata{Name: "name"}, &bq.Dataset{FriendlyName: "name"}},
191		{&DatasetMetadata{
192			Name:                   "name",
193			Description:            "desc",
194			DefaultTableExpiration: time.Hour,
195			Location:               "EU",
196			Labels:                 map[string]string{"x": "y"},
197			Access:                 []*AccessEntry{{Role: OwnerRole, Entity: "example.com", EntityType: DomainEntity}},
198		}, &bq.Dataset{
199			FriendlyName:             "name",
200			Description:              "desc",
201			DefaultTableExpirationMs: 60 * 60 * 1000,
202			Location:                 "EU",
203			Labels:                   map[string]string{"x": "y"},
204			Access:                   []*bq.DatasetAccess{{Role: "OWNER", Domain: "example.com"}},
205		}},
206	} {
207		got, err := test.in.toBQ()
208		if err != nil {
209			t.Fatal(err)
210		}
211		if !testutil.Equal(got, test.want) {
212			t.Errorf("%v:\ngot  %+v\nwant %+v", test.in, got, test.want)
213		}
214	}
215
216	// Check that non-writeable fields are unset.
217	aTime := time.Date(2017, 1, 26, 0, 0, 0, 0, time.Local)
218	for _, dm := range []*DatasetMetadata{
219		{CreationTime: aTime},
220		{LastModifiedTime: aTime},
221		{FullID: "x"},
222		{ETag: "e"},
223	} {
224		if _, err := dm.toBQ(); err == nil {
225			t.Errorf("%+v: got nil, want error", dm)
226		}
227	}
228}
229
230func TestBQToDatasetMetadata(t *testing.T) {
231	cTime := time.Date(2017, 1, 26, 0, 0, 0, 0, time.Local)
232	cMillis := cTime.UnixNano() / 1e6
233	mTime := time.Date(2017, 10, 31, 0, 0, 0, 0, time.Local)
234	mMillis := mTime.UnixNano() / 1e6
235	q := &bq.Dataset{
236		CreationTime:             cMillis,
237		LastModifiedTime:         mMillis,
238		FriendlyName:             "name",
239		Description:              "desc",
240		DefaultTableExpirationMs: 60 * 60 * 1000,
241		Location:                 "EU",
242		Labels:                   map[string]string{"x": "y"},
243		Access: []*bq.DatasetAccess{
244			{Role: "READER", UserByEmail: "joe@example.com"},
245			{Role: "WRITER", GroupByEmail: "users@example.com"},
246		},
247		Etag: "etag",
248	}
249	want := &DatasetMetadata{
250		CreationTime:           cTime,
251		LastModifiedTime:       mTime,
252		Name:                   "name",
253		Description:            "desc",
254		DefaultTableExpiration: time.Hour,
255		Location:               "EU",
256		Labels:                 map[string]string{"x": "y"},
257		Access: []*AccessEntry{
258			{Role: ReaderRole, Entity: "joe@example.com", EntityType: UserEmailEntity},
259			{Role: WriterRole, Entity: "users@example.com", EntityType: GroupEmailEntity},
260		},
261		ETag: "etag",
262	}
263	got, err := bqToDatasetMetadata(q)
264	if err != nil {
265		t.Fatal(err)
266	}
267	if diff := testutil.Diff(got, want); diff != "" {
268		t.Errorf("-got, +want:\n%s", diff)
269	}
270}
271
272func TestDatasetMetadataToUpdateToBQ(t *testing.T) {
273	dm := DatasetMetadataToUpdate{
274		Description: "desc",
275		Name:        "name",
276		DefaultTableExpiration: time.Hour,
277	}
278	dm.SetLabel("label", "value")
279	dm.DeleteLabel("del")
280
281	got, err := dm.toBQ()
282	if err != nil {
283		t.Fatal(err)
284	}
285	want := &bq.Dataset{
286		Description:              "desc",
287		FriendlyName:             "name",
288		DefaultTableExpirationMs: 60 * 60 * 1000,
289		Labels:          map[string]string{"label": "value"},
290		ForceSendFields: []string{"Description", "FriendlyName"},
291		NullFields:      []string{"Labels.del"},
292	}
293	if diff := testutil.Diff(got, want); diff != "" {
294		t.Errorf("-got, +want:\n%s", diff)
295	}
296}
297
298func TestConvertAccessEntry(t *testing.T) {
299	c := &Client{projectID: "pid"}
300	for _, e := range []*AccessEntry{
301		{Role: ReaderRole, Entity: "e", EntityType: DomainEntity},
302		{Role: WriterRole, Entity: "e", EntityType: GroupEmailEntity},
303		{Role: OwnerRole, Entity: "e", EntityType: UserEmailEntity},
304		{Role: ReaderRole, Entity: "e", EntityType: SpecialGroupEntity},
305		{Role: ReaderRole, EntityType: ViewEntity,
306			View: &Table{ProjectID: "p", DatasetID: "d", TableID: "t", c: c}},
307	} {
308		q, err := e.toBQ()
309		if err != nil {
310			t.Fatal(err)
311		}
312		got, err := bqToAccessEntry(q, c)
313		if err != nil {
314			t.Fatal(err)
315		}
316		if diff := testutil.Diff(got, e, cmp.AllowUnexported(Table{}, Client{})); diff != "" {
317			t.Errorf("got=-, want=+:\n%s", diff)
318		}
319	}
320
321	e := &AccessEntry{Role: ReaderRole, Entity: "e"}
322	if _, err := e.toBQ(); err == nil {
323		t.Error("got nil, want error")
324	}
325	if _, err := bqToAccessEntry(&bq.DatasetAccess{Role: "WRITER"}, nil); err == nil {
326		t.Error("got nil, want error")
327	}
328}
329