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