1// Copyright 2014 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 "context" 19 "crypto/tls" 20 "encoding/json" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "log" 25 "net" 26 "net/http" 27 "net/http/httptest" 28 "net/url" 29 "reflect" 30 "regexp" 31 "sort" 32 "strings" 33 "testing" 34 "time" 35 36 "cloud.google.com/go/iam" 37 "cloud.google.com/go/internal/testutil" 38 "google.golang.org/api/iterator" 39 "google.golang.org/api/option" 40 raw "google.golang.org/api/storage/v1" 41) 42 43func TestV2HeaderSanitization(t *testing.T) { 44 t.Parallel() 45 var tests = []struct { 46 desc string 47 in []string 48 want []string 49 }{ 50 { 51 desc: "already sanitized headers should not be modified", 52 in: []string{"x-goog-header1:true", "x-goog-header2:0"}, 53 want: []string{"x-goog-header1:true", "x-goog-header2:0"}, 54 }, 55 { 56 desc: "sanitized headers should be sorted", 57 in: []string{"x-goog-header2:0", "x-goog-header1:true"}, 58 want: []string{"x-goog-header1:true", "x-goog-header2:0"}, 59 }, 60 { 61 desc: "non-canonical headers should be removed", 62 in: []string{"x-goog-header1:true", "x-goog-no-value", "non-canonical-header:not-of-use"}, 63 want: []string{"x-goog-header1:true"}, 64 }, 65 { 66 desc: "excluded canonical headers should be removed", 67 in: []string{"x-goog-header1:true", "x-goog-encryption-key:my_key", "x-goog-encryption-key-sha256:my_sha256"}, 68 want: []string{"x-goog-header1:true"}, 69 }, 70 { 71 desc: "dirty headers should be formatted correctly", 72 in: []string{" x-goog-header1 : \textra-spaces ", "X-Goog-Header2:CamelCaseValue"}, 73 want: []string{"x-goog-header1:extra-spaces", "x-goog-header2:CamelCaseValue"}, 74 }, 75 { 76 desc: "duplicate headers should be merged", 77 in: []string{"x-goog-header1:value1", "X-Goog-Header1:value2"}, 78 want: []string{"x-goog-header1:value1,value2"}, 79 }, 80 } 81 for _, test := range tests { 82 got := v2SanitizeHeaders(test.in) 83 if !testutil.Equal(got, test.want) { 84 t.Errorf("%s: got %v, want %v", test.desc, got, test.want) 85 } 86 } 87} 88 89func TestV4HeaderSanitization(t *testing.T) { 90 t.Parallel() 91 var tests = []struct { 92 desc string 93 in []string 94 want []string 95 }{ 96 { 97 desc: "already sanitized headers should not be modified", 98 in: []string{"x-goog-header1:true", "x-goog-header2:0"}, 99 want: []string{"x-goog-header1:true", "x-goog-header2:0"}, 100 }, 101 { 102 desc: "dirty headers should be formatted correctly", 103 in: []string{" x-goog-header1 : \textra-spaces ", "X-Goog-Header2:CamelCaseValue"}, 104 want: []string{"x-goog-header1:extra-spaces", "x-goog-header2:CamelCaseValue"}, 105 }, 106 { 107 desc: "duplicate headers should be merged", 108 in: []string{"x-goog-header1:value1", "X-Goog-Header1:value2"}, 109 want: []string{"x-goog-header1:value1,value2"}, 110 }, 111 { 112 desc: "multiple spaces in value are stripped down to one", 113 in: []string{"foo:bar gaz"}, 114 want: []string{"foo:bar gaz"}, 115 }, 116 } 117 for _, test := range tests { 118 got := v4SanitizeHeaders(test.in) 119 sort.Strings(got) 120 sort.Strings(test.want) 121 if !testutil.Equal(got, test.want) { 122 t.Errorf("%s: got %v, want %v", test.desc, got, test.want) 123 } 124 } 125} 126 127func TestSignedURLV2(t *testing.T) { 128 expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00") 129 130 tests := []struct { 131 desc string 132 objectName string 133 opts *SignedURLOptions 134 want string 135 }{ 136 { 137 desc: "SignedURLV2 works", 138 objectName: "object-name", 139 opts: &SignedURLOptions{ 140 GoogleAccessID: "xxx@clientid", 141 PrivateKey: dummyKey("rsa"), 142 Method: "GET", 143 MD5: "ICy5YqxZB1uWSwcVLSNLcA==", 144 Expires: expires, 145 ContentType: "application/json", 146 Headers: []string{"x-goog-header1:true", "x-goog-header2:false"}, 147 }, 148 want: "https://storage.googleapis.com/bucket-name/object-name?" + 149 "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" + 150 "RfsHlPtbB2JUYjzCgNr2Mi%2BjggdEuL1V7E6N9o6aaqwVLBDuTv3I0%2B9" + 151 "x94E6rmmr%2FVgnmZigkIUxX%2Blfl7LgKf30uPGLt0mjKGH2p7r9ey1ONJ" + 152 "%2BhVec23FnTRcSgopglvHPuCMWU2oNJE%2F1y8EwWE27baHrG1RhRHbLVF" + 153 "bPpLZ9xTRFK20pluIkfHV00JGljB1imqQHXM%2B2XPWqBngLr%2FwqxLN7i" + 154 "FcUiqR8xQEOHF%2F2e7fbkTHPNq4TazaLZ8X0eZ3eFdJ55A5QmNi8atlN4W" + 155 "5q7Hvs0jcxElG3yqIbx439A995BkspLiAcA%2Fo4%2BxAwEMkGLICdbvakq" + 156 "3eEprNCojw%3D%3D", 157 }, 158 { 159 desc: "With a PEM Private Key", 160 objectName: "object-name", 161 opts: &SignedURLOptions{ 162 GoogleAccessID: "xxx@clientid", 163 PrivateKey: dummyKey("pem"), 164 Method: "GET", 165 MD5: "ICy5YqxZB1uWSwcVLSNLcA==", 166 Expires: expires, 167 ContentType: "application/json", 168 Headers: []string{"x-goog-header1:true", "x-goog-header2:false"}, 169 }, 170 want: "https://storage.googleapis.com/bucket-name/object-name?" + 171 "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" + 172 "TiyKD%2FgGb6Kh0kkb2iF%2FfF%2BnTx7L0J4YiZua8AcTmnidutePEGIU5" + 173 "NULYlrGl6l52gz4zqFb3VFfIRTcPXMdXnnFdMCDhz2QuJBUpsU1Ai9zlyTQ" + 174 "dkb6ShG03xz9%2BEXWAUQO4GBybJw%2FULASuv37xA00SwLdkqj8YdyS5II" + 175 "1lro%3D", 176 }, 177 { 178 desc: "With custom SignBytes", 179 objectName: "object-name", 180 opts: &SignedURLOptions{ 181 GoogleAccessID: "xxx@clientid", 182 SignBytes: func(b []byte) ([]byte, error) { 183 return []byte("signed"), nil 184 }, 185 Method: "GET", 186 MD5: "ICy5YqxZB1uWSwcVLSNLcA==", 187 Expires: expires, 188 ContentType: "application/json", 189 Headers: []string{"x-goog-header1:true", "x-goog-header2:false"}, 190 }, 191 want: "https://storage.googleapis.com/bucket-name/object-name?" + 192 "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" + 193 "c2lnbmVk", // base64('signed') == 'c2lnbmVk' 194 }, 195 { 196 desc: "With unsafe object name", 197 objectName: "object name界", 198 opts: &SignedURLOptions{ 199 GoogleAccessID: "xxx@clientid", 200 PrivateKey: dummyKey("pem"), 201 Method: "GET", 202 MD5: "ICy5YqxZB1uWSwcVLSNLcA==", 203 Expires: expires, 204 ContentType: "application/json", 205 Headers: []string{"x-goog-header1:true", "x-goog-header2:false"}, 206 }, 207 want: "https://storage.googleapis.com/bucket-name/object%20name%E7%95%8C?" + 208 "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=bxVH1%2Bl%2" + 209 "BSxpnj3XuqKz6mOFk6M94Y%2B4w85J6FCmJan%2FNhGSpndP6fAw1uLHlOn%2F8xUaY%2F" + 210 "SfZ5GzcQ%2BbxOL1WA37yIwZ7xgLYlO%2ByAi3GuqMUmHZiNCai28emODXQ8RtWHvgv6dE" + 211 "SQ%2F0KpDMIWW7rYCaUa63UkUyeSQsKhrVqkIA%3D", 212 }, 213 } 214 215 for _, test := range tests { 216 u, err := SignedURL("bucket-name", test.objectName, test.opts) 217 if err != nil { 218 t.Fatalf("[%s] %v", test.desc, err) 219 } 220 if u != test.want { 221 t.Fatalf("[%s] Unexpected signed URL; found %v", test.desc, u) 222 } 223 } 224} 225 226func TestSignedURLV4(t *testing.T) { 227 expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00") 228 229 tests := []struct { 230 desc string 231 objectName string 232 now time.Time 233 opts *SignedURLOptions 234 // Note for future implementors: X-Goog-Signature generated by having 235 // the client run through its algorithm with pre-defined input and copy 236 // pasting the output. These tests are not great for testing whether 237 // the right signature is calculated - instead we rely on the backend 238 // and integration tests for that. 239 want string 240 }{ 241 { 242 desc: "SignURLV4 works", 243 objectName: "object-name", 244 now: expires.Add(-24 * time.Hour), 245 opts: &SignedURLOptions{ 246 GoogleAccessID: "xxx@clientid", 247 PrivateKey: dummyKey("rsa"), 248 Method: "POST", 249 Expires: expires, 250 Scheme: SigningSchemeV4, 251 ContentType: "application/json", 252 MD5: "ICy5YqxZB1uWSwcVLSNLcA==", 253 Headers: []string{"x-goog-header1:true", "x-goog-header2:false"}, 254 }, 255 want: "https://storage.googleapis.com/bucket-name/object-name" + 256 "?X-Goog-Algorithm=GOOG4-RSA-SHA256" + 257 "&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" + 258 "&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" + 259 "&X-Goog-Signature=774b11d89663d0562b0909131b8495e70d24e31f3417d3f8fd1438a72b620b256111a7221fecab14a6ebb7dc7eed7984316a794789beb4ecdda67a77407f6de1a68113e8fa2b885e330036a995c08f0f2a7d2c212a3d0a2fd1b392d40305d3fe31ab94c547a7541278f4a956ebb6565ebe4cb27f26e30b334adb7b065adc0d27f9eaa42ee76d75d673fc4523d023d9a636de0b5329f5dffbf80024cf21fdc6236e89aa41976572bfe4807be9a9a01f644ed9f546dcf1e0394665be7610f58c36b3d63379f4d1b64f646f7427f1fc55bb89d7fdd59017d007156c99e26440e828581cddf83faf03e739e5987c062d503f2b73f24049c25edc60ecbbc09f6ce945" + 260 "&X-Goog-SignedHeaders=content-md5%3Bcontent-type%3Bhost%3Bx-goog-header1%3Bx-goog-header2", 261 }, 262 { 263 desc: "With PEM Private Key", 264 objectName: "object-name", 265 now: expires.Add(-24 * time.Hour), 266 opts: &SignedURLOptions{ 267 GoogleAccessID: "xxx@clientid", 268 PrivateKey: dummyKey("pem"), 269 Method: "GET", 270 Expires: expires, 271 Scheme: SigningSchemeV4, 272 }, 273 want: "https://storage.googleapis.com/bucket-name/object-name" + 274 "?X-Goog-Algorithm=GOOG4-RSA-SHA256" + 275 "&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" + 276 "&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" + 277 "&X-Goog-Signature=5592f4b8b2cae14025b619546d69bb463ca8f2caaab538a3cc6b5868c8c64b83a8b04b57d8a82c8696a192f62abddc8d99e0454b3fc33feac5bf87c353f0703aab6cfee60364aaeecec2edd37c1d6e6793d90812b5811b7936a014a3efad5d08477b4fbfaebf04fa61f1ca03f31bcdc46a161868cd2f4e98def6c82634a01454" + 278 "&X-Goog-SignedHeaders=host", 279 }, 280 { 281 desc: "Unsafe object name", 282 objectName: "object name界", 283 now: expires.Add(-24 * time.Hour), 284 opts: &SignedURLOptions{ 285 GoogleAccessID: "xxx@clientid", 286 PrivateKey: dummyKey("pem"), 287 Method: "GET", 288 Expires: expires, 289 Scheme: SigningSchemeV4, 290 }, 291 want: "https://storage.googleapis.com/bucket-name/object%20name%E7%95%8C" + 292 "?X-Goog-Algorithm=GOOG4-RSA-SHA256" + 293 "&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" + 294 "&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" + 295 "&X-Goog-Signature=90fd455fb47725b45c08d65ddf99078184710ad30f09bc2a190c5416ba1596e4c58420e2e48744b03de2d1b85dc8679dcb4c36af6e7a1b2547cd62becaad72aebbbaf7c1686f1aa0fedf8a9b01cef20a8b8630d824a6f8b81bb9eb75f342a7d8a28457a4efd2baac93e37089b84b1506b2af72712187f638e0eafbac650b071a" + 296 "&X-Goog-SignedHeaders=host", 297 }, 298 { 299 desc: "With custom SignBytes", 300 objectName: "object-name", 301 now: expires.Add(-24 * time.Hour), 302 opts: &SignedURLOptions{ 303 GoogleAccessID: "xxx@clientid", 304 SignBytes: func(b []byte) ([]byte, error) { 305 return []byte("signed"), nil 306 }, 307 Method: "GET", 308 Expires: expires, 309 Scheme: SigningSchemeV4, 310 }, 311 want: "https://storage.googleapis.com/bucket-name/object-name" + 312 "?X-Goog-Algorithm=GOOG4-RSA-SHA256" + 313 "&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" + 314 "&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" + 315 "&X-Goog-Signature=7369676e6564" + // hex('signed') = '7369676e6564' 316 "&X-Goog-SignedHeaders=host", 317 }, 318 } 319 oldUTCNow := utcNow 320 defer func() { 321 utcNow = oldUTCNow 322 }() 323 324 for _, test := range tests { 325 t.Logf("Testcase: '%s'", test.desc) 326 327 utcNow = func() time.Time { 328 return test.now 329 } 330 got, err := SignedURL("bucket-name", test.objectName, test.opts) 331 if err != nil { 332 t.Fatal(err) 333 } 334 if got != test.want { 335 t.Fatalf("\n\tgot:\t%v\n\twant:\t%v", got, test.want) 336 } 337 } 338} 339 340func TestSignedURL_MissingOptions(t *testing.T) { 341 now, _ := time.Parse(time.RFC3339, "2002-10-01T00:00:00-05:00") 342 expires, _ := time.Parse(time.RFC3339, "2002-10-15T00:00:00-05:00") 343 pk := dummyKey("rsa") 344 345 var tests = []struct { 346 opts *SignedURLOptions 347 errMsg string 348 }{ 349 { 350 &SignedURLOptions{}, 351 "missing required GoogleAccessID", 352 }, 353 { 354 &SignedURLOptions{GoogleAccessID: "access_id"}, 355 "exactly one of PrivateKey or SignedBytes must be set", 356 }, 357 { 358 &SignedURLOptions{ 359 GoogleAccessID: "access_id", 360 SignBytes: func(b []byte) ([]byte, error) { return b, nil }, 361 PrivateKey: pk, 362 }, 363 "exactly one of PrivateKey or SignedBytes must be set", 364 }, 365 { 366 &SignedURLOptions{ 367 GoogleAccessID: "access_id", 368 PrivateKey: pk, 369 }, 370 errMethodNotValid.Error(), 371 }, 372 { 373 &SignedURLOptions{ 374 GoogleAccessID: "access_id", 375 PrivateKey: pk, 376 Method: "getMethod", // wrong method name 377 }, 378 errMethodNotValid.Error(), 379 }, 380 { 381 &SignedURLOptions{ 382 GoogleAccessID: "access_id", 383 PrivateKey: pk, 384 Method: "get", // name will be uppercased 385 }, 386 "missing required expires", 387 }, 388 { 389 &SignedURLOptions{ 390 GoogleAccessID: "access_id", 391 SignBytes: func(b []byte) ([]byte, error) { return b, nil }, 392 }, 393 errMethodNotValid.Error(), 394 }, 395 { 396 &SignedURLOptions{ 397 GoogleAccessID: "access_id", 398 PrivateKey: pk, 399 Method: "PUT", 400 }, 401 "missing required expires", 402 }, 403 { 404 &SignedURLOptions{ 405 GoogleAccessID: "access_id", 406 PrivateKey: pk, 407 Method: "PUT", 408 Expires: expires, 409 MD5: "invalid", 410 }, 411 "invalid MD5 checksum", 412 }, 413 // SigningSchemeV4 tests 414 { 415 &SignedURLOptions{ 416 PrivateKey: pk, 417 Method: "GET", 418 Expires: expires, 419 Scheme: SigningSchemeV4, 420 }, 421 "missing required GoogleAccessID", 422 }, 423 { 424 &SignedURLOptions{ 425 GoogleAccessID: "access_id", 426 Method: "GET", 427 Expires: expires, 428 SignBytes: func(b []byte) ([]byte, error) { return b, nil }, 429 PrivateKey: pk, 430 Scheme: SigningSchemeV4, 431 }, 432 "exactly one of PrivateKey or SignedBytes must be set", 433 }, 434 { 435 &SignedURLOptions{ 436 GoogleAccessID: "access_id", 437 PrivateKey: pk, 438 Expires: expires, 439 Scheme: SigningSchemeV4, 440 }, 441 errMethodNotValid.Error(), 442 }, 443 { 444 &SignedURLOptions{ 445 GoogleAccessID: "access_id", 446 PrivateKey: pk, 447 Method: "PUT", 448 Scheme: SigningSchemeV4, 449 }, 450 "missing required expires", 451 }, 452 { 453 &SignedURLOptions{ 454 GoogleAccessID: "access_id", 455 PrivateKey: pk, 456 Method: "PUT", 457 Expires: now.Add(time.Hour), 458 MD5: "invalid", 459 Scheme: SigningSchemeV4, 460 }, 461 "invalid MD5 checksum", 462 }, 463 { 464 &SignedURLOptions{ 465 GoogleAccessID: "access_id", 466 PrivateKey: pk, 467 Method: "GET", 468 Expires: expires, 469 Scheme: SigningSchemeV4, 470 }, 471 "expires must be within seven days from now", 472 }, 473 } 474 oldUTCNow := utcNow 475 defer func() { 476 utcNow = oldUTCNow 477 }() 478 utcNow = func() time.Time { 479 return now 480 } 481 482 for _, test := range tests { 483 _, err := SignedURL("bucket", "name", test.opts) 484 if !strings.Contains(err.Error(), test.errMsg) { 485 t.Errorf("expected err: %v, found: %v", test.errMsg, err) 486 } 487 } 488} 489 490func TestPathEncodeV4(t *testing.T) { 491 tests := []struct { 492 input string 493 want string 494 }{ 495 { 496 "path/with/slashes", 497 "path/with/slashes", 498 }, 499 { 500 "path/with/speci@lchar$&", 501 "path/with/speci%40lchar%24%26", 502 }, 503 { 504 "path/with/un_ersc_re/~tilde/sp ace/", 505 "path/with/un_ersc_re/~tilde/sp%20%20ace/", 506 }, 507 } 508 for _, test := range tests { 509 if got := pathEncodeV4(test.input); got != test.want { 510 t.Errorf("pathEncodeV4(%q) = %q, want %q", test.input, got, test.want) 511 } 512 } 513} 514 515func dummyKey(kind string) []byte { 516 slurp, err := ioutil.ReadFile(fmt.Sprintf("./internal/test/dummy_%s", kind)) 517 if err != nil { 518 log.Fatal(err) 519 } 520 return slurp 521} 522 523func TestObjectNames(t *testing.T) { 524 t.Parallel() 525 // Naming requirements: https://cloud.google.com/storage/docs/bucket-naming 526 const maxLegalLength = 1024 527 528 type testT struct { 529 name, want string 530 } 531 tests := []testT{ 532 // Embedded characters important in URLs. 533 {"foo % bar", "foo%20%25%20bar"}, 534 {"foo ? bar", "foo%20%3F%20bar"}, 535 {"foo / bar", "foo%20/%20bar"}, 536 {"foo %?/ bar", "foo%20%25%3F/%20bar"}, 537 538 // Non-Roman scripts 539 {"타코", "%ED%83%80%EC%BD%94"}, 540 {"世界", "%E4%B8%96%E7%95%8C"}, 541 542 // Longest legal name 543 {strings.Repeat("a", maxLegalLength), strings.Repeat("a", maxLegalLength)}, 544 545 // Line terminators besides CR and LF: https://en.wikipedia.org/wiki/Newline#Unicode 546 {"foo \u000b bar", "foo%20%0B%20bar"}, 547 {"foo \u000c bar", "foo%20%0C%20bar"}, 548 {"foo \u0085 bar", "foo%20%C2%85%20bar"}, 549 {"foo \u2028 bar", "foo%20%E2%80%A8%20bar"}, 550 {"foo \u2029 bar", "foo%20%E2%80%A9%20bar"}, 551 552 // Null byte. 553 {"foo \u0000 bar", "foo%20%00%20bar"}, 554 555 // Non-control characters that are discouraged, but not forbidden, according to the documentation. 556 {"foo # bar", "foo%20%23%20bar"}, 557 {"foo []*? bar", "foo%20%5B%5D%2A%3F%20bar"}, 558 559 // Angstrom symbol singleton and normalized forms: http://unicode.org/reports/tr15/ 560 {"foo \u212b bar", "foo%20%E2%84%AB%20bar"}, 561 {"foo \u0041\u030a bar", "foo%20A%CC%8A%20bar"}, 562 {"foo \u00c5 bar", "foo%20%C3%85%20bar"}, 563 564 // Hangul separating jamo: http://www.unicode.org/versions/Unicode7.0.0/ch18.pdf (Table 18-10) 565 {"foo \u3131\u314f bar", "foo%20%E3%84%B1%E3%85%8F%20bar"}, 566 {"foo \u1100\u1161 bar", "foo%20%E1%84%80%E1%85%A1%20bar"}, 567 {"foo \uac00 bar", "foo%20%EA%B0%80%20bar"}, 568 } 569 570 // C0 control characters not forbidden by the docs. 571 var runes []rune 572 for r := rune(0x01); r <= rune(0x1f); r++ { 573 if r != '\u000a' && r != '\u000d' { 574 runes = append(runes, r) 575 } 576 } 577 tests = append(tests, testT{fmt.Sprintf("foo %s bar", string(runes)), "foo%20%01%02%03%04%05%06%07%08%09%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20bar"}) 578 579 // C1 control characters, plus DEL. 580 runes = nil 581 for r := rune(0x7f); r <= rune(0x9f); r++ { 582 runes = append(runes, r) 583 } 584 tests = append(tests, testT{fmt.Sprintf("foo %s bar", string(runes)), "foo%20%7F%C2%80%C2%81%C2%82%C2%83%C2%84%C2%85%C2%86%C2%87%C2%88%C2%89%C2%8A%C2%8B%C2%8C%C2%8D%C2%8E%C2%8F%C2%90%C2%91%C2%92%C2%93%C2%94%C2%95%C2%96%C2%97%C2%98%C2%99%C2%9A%C2%9B%C2%9C%C2%9D%C2%9E%C2%9F%20bar"}) 585 586 opts := &SignedURLOptions{ 587 GoogleAccessID: "xxx@clientid", 588 PrivateKey: dummyKey("rsa"), 589 Method: "GET", 590 MD5: "ICy5YqxZB1uWSwcVLSNLcA==", 591 Expires: time.Date(2002, time.October, 2, 10, 0, 0, 0, time.UTC), 592 ContentType: "application/json", 593 Headers: []string{"x-goog-header1", "x-goog-header2"}, 594 } 595 596 for _, test := range tests { 597 g, err := SignedURL("bucket-name", test.name, opts) 598 if err != nil { 599 t.Errorf("SignedURL(%q) err=%v, want nil", test.name, err) 600 } 601 if w := "/bucket-name/" + test.want; !strings.Contains(g, w) { 602 t.Errorf("SignedURL(%q)=%q, want substring %q", test.name, g, w) 603 } 604 } 605} 606 607func TestCondition(t *testing.T) { 608 t.Parallel() 609 gotReq := make(chan *http.Request, 1) 610 hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) { 611 io.Copy(ioutil.Discard, r.Body) 612 gotReq <- r 613 w.WriteHeader(200) 614 }) 615 defer close() 616 ctx := context.Background() 617 c, err := NewClient(ctx, option.WithHTTPClient(hc)) 618 if err != nil { 619 t.Fatal(err) 620 } 621 622 obj := c.Bucket("buck").Object("obj") 623 dst := c.Bucket("dstbuck").Object("dst") 624 tests := []struct { 625 fn func() error 626 want string 627 }{ 628 { 629 func() error { 630 _, err := obj.Generation(1234).NewReader(ctx) 631 return err 632 }, 633 "GET /buck/obj?generation=1234", 634 }, 635 { 636 func() error { 637 _, err := obj.If(Conditions{GenerationMatch: 1234}).NewReader(ctx) 638 return err 639 }, 640 "GET /buck/obj?ifGenerationMatch=1234", 641 }, 642 { 643 func() error { 644 _, err := obj.If(Conditions{GenerationNotMatch: 1234}).NewReader(ctx) 645 return err 646 }, 647 "GET /buck/obj?ifGenerationNotMatch=1234", 648 }, 649 { 650 func() error { 651 _, err := obj.If(Conditions{MetagenerationMatch: 1234}).NewReader(ctx) 652 return err 653 }, 654 "GET /buck/obj?ifMetagenerationMatch=1234", 655 }, 656 { 657 func() error { 658 _, err := obj.If(Conditions{MetagenerationNotMatch: 1234}).NewReader(ctx) 659 return err 660 }, 661 "GET /buck/obj?ifMetagenerationNotMatch=1234", 662 }, 663 { 664 func() error { 665 _, err := obj.If(Conditions{MetagenerationNotMatch: 1234}).Attrs(ctx) 666 return err 667 }, 668 "GET /storage/v1/b/buck/o/obj?alt=json&ifMetagenerationNotMatch=1234&prettyPrint=false&projection=full", 669 }, 670 { 671 func() error { 672 _, err := obj.If(Conditions{MetagenerationMatch: 1234}).Update(ctx, ObjectAttrsToUpdate{}) 673 return err 674 }, 675 "PATCH /storage/v1/b/buck/o/obj?alt=json&ifMetagenerationMatch=1234&prettyPrint=false&projection=full", 676 }, 677 { 678 func() error { return obj.Generation(1234).Delete(ctx) }, 679 "DELETE /storage/v1/b/buck/o/obj?alt=json&generation=1234&prettyPrint=false", 680 }, 681 { 682 func() error { 683 w := obj.If(Conditions{GenerationMatch: 1234}).NewWriter(ctx) 684 w.ContentType = "text/plain" 685 return w.Close() 686 }, 687 "POST /upload/storage/v1/b/buck/o?alt=json&ifGenerationMatch=1234&prettyPrint=false&projection=full&uploadType=multipart", 688 }, 689 { 690 func() error { 691 w := obj.If(Conditions{DoesNotExist: true}).NewWriter(ctx) 692 w.ContentType = "text/plain" 693 return w.Close() 694 }, 695 "POST /upload/storage/v1/b/buck/o?alt=json&ifGenerationMatch=0&prettyPrint=false&projection=full&uploadType=multipart", 696 }, 697 { 698 func() error { 699 _, err := dst.If(Conditions{MetagenerationMatch: 5678}).CopierFrom(obj.If(Conditions{GenerationMatch: 1234})).Run(ctx) 700 return err 701 }, 702 "POST /storage/v1/b/buck/o/obj/rewriteTo/b/dstbuck/o/dst?alt=json&ifMetagenerationMatch=5678&ifSourceGenerationMatch=1234&prettyPrint=false&projection=full", 703 }, 704 } 705 706 for i, tt := range tests { 707 if err := tt.fn(); err != nil && err != io.EOF { 708 t.Error(err) 709 continue 710 } 711 select { 712 case r := <-gotReq: 713 got := r.Method + " " + r.RequestURI 714 if got != tt.want { 715 t.Errorf("%d. RequestURI = %q; want %q", i, got, tt.want) 716 } 717 case <-time.After(5 * time.Second): 718 t.Fatalf("%d. timeout", i) 719 } 720 if err != nil { 721 t.Fatal(err) 722 } 723 } 724 725 // Test an error, too: 726 err = obj.Generation(1234).NewWriter(ctx).Close() 727 if err == nil || !strings.Contains(err.Error(), "NewWriter: generation not supported") { 728 t.Errorf("want error about unsupported generation; got %v", err) 729 } 730} 731 732func TestConditionErrors(t *testing.T) { 733 t.Parallel() 734 for _, conds := range []Conditions{ 735 {GenerationMatch: 0}, 736 {DoesNotExist: false}, // same as above, actually 737 {GenerationMatch: 1, GenerationNotMatch: 2}, 738 {GenerationNotMatch: 2, DoesNotExist: true}, 739 {MetagenerationMatch: 1, MetagenerationNotMatch: 2}, 740 } { 741 if err := conds.validate(""); err == nil { 742 t.Errorf("%+v: got nil, want error", conds) 743 } 744 } 745} 746 747// Test object compose. 748func TestObjectCompose(t *testing.T) { 749 t.Parallel() 750 gotURL := make(chan string, 1) 751 gotBody := make(chan []byte, 1) 752 hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) { 753 body, _ := ioutil.ReadAll(r.Body) 754 gotURL <- r.URL.String() 755 gotBody <- body 756 w.Write([]byte("{}")) 757 }) 758 defer close() 759 ctx := context.Background() 760 c, err := NewClient(ctx, option.WithHTTPClient(hc)) 761 if err != nil { 762 t.Fatal(err) 763 } 764 765 testCases := []struct { 766 desc string 767 dst *ObjectHandle 768 srcs []*ObjectHandle 769 attrs *ObjectAttrs 770 wantReq raw.ComposeRequest 771 wantURL string 772 wantErr bool 773 }{ 774 { 775 desc: "basic case", 776 dst: c.Bucket("foo").Object("bar"), 777 srcs: []*ObjectHandle{ 778 c.Bucket("foo").Object("baz"), 779 c.Bucket("foo").Object("quux"), 780 }, 781 wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json&prettyPrint=false", 782 wantReq: raw.ComposeRequest{ 783 Destination: &raw.Object{Bucket: "foo"}, 784 SourceObjects: []*raw.ComposeRequestSourceObjects{ 785 {Name: "baz"}, 786 {Name: "quux"}, 787 }, 788 }, 789 }, 790 { 791 desc: "with object attrs", 792 dst: c.Bucket("foo").Object("bar"), 793 srcs: []*ObjectHandle{ 794 c.Bucket("foo").Object("baz"), 795 c.Bucket("foo").Object("quux"), 796 }, 797 attrs: &ObjectAttrs{ 798 Name: "not-bar", 799 ContentType: "application/json", 800 }, 801 wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json&prettyPrint=false", 802 wantReq: raw.ComposeRequest{ 803 Destination: &raw.Object{ 804 Bucket: "foo", 805 Name: "not-bar", 806 ContentType: "application/json", 807 }, 808 SourceObjects: []*raw.ComposeRequestSourceObjects{ 809 {Name: "baz"}, 810 {Name: "quux"}, 811 }, 812 }, 813 }, 814 { 815 desc: "with conditions", 816 dst: c.Bucket("foo").Object("bar").If(Conditions{ 817 GenerationMatch: 12, 818 MetagenerationMatch: 34, 819 }), 820 srcs: []*ObjectHandle{ 821 c.Bucket("foo").Object("baz").Generation(56), 822 c.Bucket("foo").Object("quux").If(Conditions{GenerationMatch: 78}), 823 }, 824 wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json&ifGenerationMatch=12&ifMetagenerationMatch=34&prettyPrint=false", 825 wantReq: raw.ComposeRequest{ 826 Destination: &raw.Object{Bucket: "foo"}, 827 SourceObjects: []*raw.ComposeRequestSourceObjects{ 828 { 829 Name: "baz", 830 Generation: 56, 831 }, 832 { 833 Name: "quux", 834 ObjectPreconditions: &raw.ComposeRequestSourceObjectsObjectPreconditions{ 835 IfGenerationMatch: 78, 836 }, 837 }, 838 }, 839 }, 840 }, 841 { 842 desc: "no sources", 843 dst: c.Bucket("foo").Object("bar"), 844 wantErr: true, 845 }, 846 { 847 desc: "destination, no bucket", 848 dst: c.Bucket("").Object("bar"), 849 srcs: []*ObjectHandle{ 850 c.Bucket("foo").Object("baz"), 851 }, 852 wantErr: true, 853 }, 854 { 855 desc: "destination, no object", 856 dst: c.Bucket("foo").Object(""), 857 srcs: []*ObjectHandle{ 858 c.Bucket("foo").Object("baz"), 859 }, 860 wantErr: true, 861 }, 862 { 863 desc: "source, different bucket", 864 dst: c.Bucket("foo").Object("bar"), 865 srcs: []*ObjectHandle{ 866 c.Bucket("otherbucket").Object("baz"), 867 }, 868 wantErr: true, 869 }, 870 { 871 desc: "source, no object", 872 dst: c.Bucket("foo").Object("bar"), 873 srcs: []*ObjectHandle{ 874 c.Bucket("foo").Object(""), 875 }, 876 wantErr: true, 877 }, 878 { 879 desc: "destination, bad condition", 880 dst: c.Bucket("foo").Object("bar").Generation(12), 881 srcs: []*ObjectHandle{ 882 c.Bucket("foo").Object("baz"), 883 }, 884 wantErr: true, 885 }, 886 { 887 desc: "source, bad condition", 888 dst: c.Bucket("foo").Object("bar"), 889 srcs: []*ObjectHandle{ 890 c.Bucket("foo").Object("baz").If(Conditions{MetagenerationMatch: 12}), 891 }, 892 wantErr: true, 893 }, 894 } 895 896 for _, tt := range testCases { 897 composer := tt.dst.ComposerFrom(tt.srcs...) 898 if tt.attrs != nil { 899 composer.ObjectAttrs = *tt.attrs 900 } 901 _, err := composer.Run(ctx) 902 if gotErr := err != nil; gotErr != tt.wantErr { 903 t.Errorf("%s: got error %v; want err %t", tt.desc, err, tt.wantErr) 904 continue 905 } 906 if tt.wantErr { 907 continue 908 } 909 u, body := <-gotURL, <-gotBody 910 if u != tt.wantURL { 911 t.Errorf("%s: request URL\ngot %q\nwant %q", tt.desc, u, tt.wantURL) 912 } 913 var req raw.ComposeRequest 914 if err := json.Unmarshal(body, &req); err != nil { 915 t.Errorf("%s: json.Unmarshal %v (body %s)", tt.desc, err, body) 916 } 917 if !testutil.Equal(req, tt.wantReq) { 918 // Print to JSON. 919 wantReq, _ := json.Marshal(tt.wantReq) 920 t.Errorf("%s: request body\ngot %s\nwant %s", tt.desc, body, wantReq) 921 } 922 } 923} 924 925// Test that ObjectIterator's Next and NextPage methods correctly terminate 926// if there is nothing to iterate over. 927func TestEmptyObjectIterator(t *testing.T) { 928 t.Parallel() 929 hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) { 930 io.Copy(ioutil.Discard, r.Body) 931 fmt.Fprintf(w, "{}") 932 }) 933 defer close() 934 ctx := context.Background() 935 client, err := NewClient(ctx, option.WithHTTPClient(hClient)) 936 if err != nil { 937 t.Fatal(err) 938 } 939 it := client.Bucket("b").Objects(ctx, nil) 940 _, err = it.Next() 941 if err != iterator.Done { 942 t.Errorf("got %v, want Done", err) 943 } 944} 945 946// Test that BucketIterator's Next method correctly terminates if there is 947// nothing to iterate over. 948func TestEmptyBucketIterator(t *testing.T) { 949 t.Parallel() 950 hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) { 951 io.Copy(ioutil.Discard, r.Body) 952 fmt.Fprintf(w, "{}") 953 }) 954 defer close() 955 ctx := context.Background() 956 client, err := NewClient(ctx, option.WithHTTPClient(hClient)) 957 if err != nil { 958 t.Fatal(err) 959 } 960 it := client.Buckets(ctx, "project") 961 _, err = it.Next() 962 if err != iterator.Done { 963 t.Errorf("got %v, want Done", err) 964 } 965 966} 967 968func TestCodecUint32(t *testing.T) { 969 t.Parallel() 970 for _, u := range []uint32{0, 1, 256, 0xFFFFFFFF} { 971 s := encodeUint32(u) 972 d, err := decodeUint32(s) 973 if err != nil { 974 t.Fatal(err) 975 } 976 if d != u { 977 t.Errorf("got %d, want input %d", d, u) 978 } 979 } 980} 981 982func TestUserProject(t *testing.T) { 983 // Verify that the userProject query param is sent. 984 t.Parallel() 985 ctx := context.Background() 986 gotURL := make(chan *url.URL, 1) 987 hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) { 988 io.Copy(ioutil.Discard, r.Body) 989 gotURL <- r.URL 990 if strings.Contains(r.URL.String(), "/rewriteTo/") { 991 res := &raw.RewriteResponse{Done: true} 992 bytes, err := res.MarshalJSON() 993 if err != nil { 994 t.Fatal(err) 995 } 996 w.Write(bytes) 997 } else { 998 fmt.Fprintf(w, "{}") 999 } 1000 }) 1001 defer close() 1002 client, err := NewClient(ctx, option.WithHTTPClient(hClient)) 1003 if err != nil { 1004 t.Fatal(err) 1005 } 1006 1007 re := regexp.MustCompile(`\buserProject=p\b`) 1008 b := client.Bucket("b").UserProject("p") 1009 o := b.Object("o") 1010 1011 check := func(msg string, f func()) { 1012 f() 1013 select { 1014 case u := <-gotURL: 1015 if !re.MatchString(u.RawQuery) { 1016 t.Errorf("%s: query string %q does not contain userProject", msg, u.RawQuery) 1017 } 1018 case <-time.After(2 * time.Second): 1019 t.Errorf("%s: timed out", msg) 1020 } 1021 } 1022 1023 check("buckets.delete", func() { b.Delete(ctx) }) 1024 check("buckets.get", func() { b.Attrs(ctx) }) 1025 check("buckets.patch", func() { b.Update(ctx, BucketAttrsToUpdate{}) }) 1026 check("storage.objects.compose", func() { o.ComposerFrom(b.Object("x")).Run(ctx) }) 1027 check("storage.objects.delete", func() { o.Delete(ctx) }) 1028 check("storage.objects.get", func() { o.Attrs(ctx) }) 1029 check("storage.objects.insert", func() { o.NewWriter(ctx).Close() }) 1030 check("storage.objects.list", func() { b.Objects(ctx, nil).Next() }) 1031 check("storage.objects.patch", func() { o.Update(ctx, ObjectAttrsToUpdate{}) }) 1032 check("storage.objects.rewrite", func() { o.CopierFrom(b.Object("x")).Run(ctx) }) 1033 check("storage.objectAccessControls.list", func() { o.ACL().List(ctx) }) 1034 check("storage.objectAccessControls.update", func() { o.ACL().Set(ctx, "", "") }) 1035 check("storage.objectAccessControls.delete", func() { o.ACL().Delete(ctx, "") }) 1036 check("storage.bucketAccessControls.list", func() { b.ACL().List(ctx) }) 1037 check("storage.bucketAccessControls.update", func() { b.ACL().Set(ctx, "", "") }) 1038 check("storage.bucketAccessControls.delete", func() { b.ACL().Delete(ctx, "") }) 1039 check("storage.defaultObjectAccessControls.list", 1040 func() { b.DefaultObjectACL().List(ctx) }) 1041 check("storage.defaultObjectAccessControls.update", 1042 func() { b.DefaultObjectACL().Set(ctx, "", "") }) 1043 check("storage.defaultObjectAccessControls.delete", 1044 func() { b.DefaultObjectACL().Delete(ctx, "") }) 1045 check("buckets.getIamPolicy", func() { b.IAM().Policy(ctx) }) 1046 check("buckets.setIamPolicy", func() { 1047 p := &iam.Policy{} 1048 p.Add("m", iam.Owner) 1049 b.IAM().SetPolicy(ctx, p) 1050 }) 1051 check("buckets.testIamPermissions", func() { b.IAM().TestPermissions(ctx, nil) }) 1052 check("storage.notifications.insert", func() { 1053 b.AddNotification(ctx, &Notification{TopicProjectID: "p", TopicID: "t"}) 1054 }) 1055 check("storage.notifications.delete", func() { b.DeleteNotification(ctx, "n") }) 1056 check("storage.notifications.list", func() { b.Notifications(ctx) }) 1057} 1058 1059func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*http.Client, func()) { 1060 ts := httptest.NewTLSServer(http.HandlerFunc(handler)) 1061 tlsConf := &tls.Config{InsecureSkipVerify: true} 1062 tr := &http.Transport{ 1063 TLSClientConfig: tlsConf, 1064 DialTLS: func(netw, addr string) (net.Conn, error) { 1065 return tls.Dial("tcp", ts.Listener.Addr().String(), tlsConf) 1066 }, 1067 } 1068 return &http.Client{Transport: tr}, func() { 1069 tr.CloseIdleConnections() 1070 ts.Close() 1071 } 1072} 1073 1074func TestRawObjectToObjectAttrs(t *testing.T) { 1075 t.Parallel() 1076 tests := []struct { 1077 in *raw.Object 1078 want *ObjectAttrs 1079 }{ 1080 {in: nil, want: nil}, 1081 { 1082 in: &raw.Object{ 1083 Bucket: "Test", 1084 ContentLanguage: "en-us", 1085 ContentType: "video/mpeg", 1086 EventBasedHold: false, 1087 Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0", 1088 Generation: 7, 1089 Md5Hash: "MTQ2ODNjYmE0NDRkYmNjNmRiMjk3NjQ1ZTY4M2Y1YzE=", 1090 Name: "foo.mp4", 1091 RetentionExpirationTime: "2019-03-31T19:33:36Z", 1092 Size: 1 << 20, 1093 TimeCreated: "2019-03-31T19:32:10Z", 1094 TimeDeleted: "2019-03-31T19:33:39Z", 1095 TemporaryHold: true, 1096 }, 1097 want: &ObjectAttrs{ 1098 Bucket: "Test", 1099 Created: time.Date(2019, 3, 31, 19, 32, 10, 0, time.UTC), 1100 ContentLanguage: "en-us", 1101 ContentType: "video/mpeg", 1102 Deleted: time.Date(2019, 3, 31, 19, 33, 39, 0, time.UTC), 1103 EventBasedHold: false, 1104 Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0", 1105 Generation: 7, 1106 MD5: []byte("14683cba444dbcc6db297645e683f5c1"), 1107 Name: "foo.mp4", 1108 RetentionExpirationTime: time.Date(2019, 3, 31, 19, 33, 36, 0, time.UTC), 1109 Size: 1 << 20, 1110 TemporaryHold: true, 1111 }, 1112 }, 1113 } 1114 1115 for i, tt := range tests { 1116 got := newObject(tt.in) 1117 if diff := testutil.Diff(got, tt.want); diff != "" { 1118 t.Errorf("#%d: newObject mismatches:\ngot=-, want=+:\n%s", i, diff) 1119 } 1120 } 1121} 1122 1123func TestObjectAttrsToRawObject(t *testing.T) { 1124 t.Parallel() 1125 bucketName := "the-bucket" 1126 in := &ObjectAttrs{ 1127 Bucket: "Test", 1128 Created: time.Date(2019, 3, 31, 19, 32, 10, 0, time.UTC), 1129 ContentLanguage: "en-us", 1130 ContentType: "video/mpeg", 1131 Deleted: time.Date(2019, 3, 31, 19, 33, 39, 0, time.UTC), 1132 EventBasedHold: false, 1133 Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0", 1134 Generation: 7, 1135 MD5: []byte("14683cba444dbcc6db297645e683f5c1"), 1136 Name: "foo.mp4", 1137 RetentionExpirationTime: time.Date(2019, 3, 31, 19, 33, 36, 0, time.UTC), 1138 Size: 1 << 20, 1139 TemporaryHold: true, 1140 } 1141 want := &raw.Object{ 1142 Bucket: bucketName, 1143 ContentLanguage: "en-us", 1144 ContentType: "video/mpeg", 1145 EventBasedHold: false, 1146 Name: "foo.mp4", 1147 RetentionExpirationTime: "2019-03-31T19:33:36Z", 1148 TemporaryHold: true, 1149 } 1150 got := in.toRawObject(bucketName) 1151 if !testutil.Equal(got, want) { 1152 if diff := testutil.Diff(got, want); diff != "" { 1153 t.Errorf("toRawObject mismatches:\ngot=-, want=+:\n%s", diff) 1154 } 1155 } 1156} 1157 1158func TestAttrToFieldMapCoverage(t *testing.T) { 1159 t.Parallel() 1160 1161 oa := reflect.TypeOf((*ObjectAttrs)(nil)).Elem() 1162 oaFields := make(map[string]bool) 1163 1164 for i := 0; i < oa.NumField(); i++ { 1165 fieldName := oa.Field(i).Name 1166 oaFields[fieldName] = true 1167 } 1168 1169 // Check that all fields of attrToFieldMap exist in ObjectAttrs. 1170 for k := range attrToFieldMap { 1171 if _, ok := oaFields[k]; !ok { 1172 t.Errorf("%v is not an ObjectAttrs field", k) 1173 } 1174 } 1175 1176 // Check that all fields of ObjectAttrs exist in attrToFieldMap, with 1177 // known exceptions which aren't sent over the wire but are settable by 1178 // the user. 1179 for k := range oaFields { 1180 if _, ok := attrToFieldMap[k]; !ok { 1181 if k != "Prefix" && k != "PredefinedACL" { 1182 t.Errorf("ObjectAttrs.%v is not in attrToFieldMap", k) 1183 } 1184 } 1185 } 1186} 1187 1188// Create a client using a custom endpoint, and verify that raw.BasePath (used 1189// for writes) and readHost (used for reads) are both set correctly. 1190func TestWithEndpoint(t *testing.T) { 1191 ctx := context.Background() 1192 endpoint := "https://fake.gcs.com:8080/storage/v1" 1193 c, err := NewClient(ctx, option.WithEndpoint(endpoint)) 1194 if err != nil { 1195 t.Fatalf("error creating client: %v", err) 1196 } 1197 1198 if c.raw.BasePath != endpoint { 1199 t.Errorf("raw.BasePath not set correctly: got %v, want %v", c.raw.BasePath, endpoint) 1200 } 1201 1202 want := "fake.gcs.com:8080" 1203 if c.readHost != want { 1204 t.Errorf("readHost not set correctly: got %v, want %v", c.readHost, want) 1205 } 1206} 1207