1// Copyright 2016, the Blazer authors 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 base 16 17import ( 18 "bytes" 19 "crypto/sha1" 20 "encoding/json" 21 "fmt" 22 "io" 23 "os" 24 "reflect" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/kurin/blazer/x/transport" 30 31 "context" 32) 33 34const ( 35 apiID = "B2_ACCOUNT_ID" 36 apiKey = "B2_SECRET_KEY" 37) 38 39const ( 40 bucketName = "base-tests" 41 smallFileName = "TeenyTiny" 42 largeFileName = "BigBytes" 43) 44 45type zReader struct{} 46 47func (zReader) Read(p []byte) (int, error) { 48 return len(p), nil 49} 50 51func TestStorage(t *testing.T) { 52 id := os.Getenv(apiID) 53 key := os.Getenv(apiKey) 54 if id == "" || key == "" { 55 t.Skipf("B2_ACCOUNT_ID or B2_SECRET_KEY unset; skipping integration tests") 56 } 57 ctx := context.Background() 58 59 // b2_authorize_account 60 b2, err := AuthorizeAccount(ctx, id, key, UserAgent("blazer-base-test")) 61 if err != nil { 62 t.Fatal(err) 63 } 64 65 // b2_create_bucket 66 infoKey := "key" 67 infoVal := "val" 68 m := map[string]string{infoKey: infoVal} 69 rules := []LifecycleRule{ 70 { 71 Prefix: "what/", 72 DaysNewUntilHidden: 5, 73 }, 74 } 75 bname := id + "-" + bucketName 76 bucket, err := b2.CreateBucket(ctx, bname, "", m, rules) 77 if err != nil { 78 t.Fatal(err) 79 } 80 if bucket.Info[infoKey] != infoVal { 81 t.Errorf("%s: bucketInfo[%q] got %q, want %q", bucket.Name, infoKey, bucket.Info[infoKey], infoVal) 82 } 83 if len(bucket.LifecycleRules) != 1 { 84 t.Errorf("%s: lifecycle rules: got %d rules, wanted 1", bucket.Name, len(bucket.LifecycleRules)) 85 } 86 87 defer func() { 88 // b2_delete_bucket 89 if err := bucket.DeleteBucket(ctx); err != nil { 90 t.Error(err) 91 } 92 }() 93 94 // b2_update_bucket 95 bucket.Info["new"] = "yay" 96 bucket.LifecycleRules = nil // Unset options should be a noop. 97 newBucket, err := bucket.Update(ctx) 98 if err != nil { 99 t.Errorf("%s: update bucket: %v", bucket.Name, err) 100 return 101 } 102 bucket = newBucket 103 if bucket.Info["new"] != "yay" { 104 t.Errorf("%s: info key \"new\": got %s, want \"yay\"", bucket.Name, bucket.Info["new"]) 105 } 106 if len(bucket.LifecycleRules) != 1 { 107 t.Errorf("%s: lifecycle rules: got %d rules, wanted 1", bucket.Name, len(bucket.LifecycleRules)) 108 } 109 110 // b2_list_buckets 111 buckets, err := b2.ListBuckets(ctx) 112 if err != nil { 113 t.Fatal(err) 114 } 115 var found bool 116 for _, bucket := range buckets { 117 if bucket.Name == bname { 118 found = true 119 break 120 } 121 } 122 if !found { 123 t.Errorf("%s: new bucket not found", bname) 124 } 125 126 // b2_get_upload_url 127 ue, err := bucket.GetUploadURL(ctx) 128 if err != nil { 129 t.Fatal(err) 130 } 131 132 // b2_upload_file 133 smallFile := io.LimitReader(zReader{}, 1024*50) // 50k 134 hash := sha1.New() 135 buf := &bytes.Buffer{} 136 w := io.MultiWriter(hash, buf) 137 if _, err := io.Copy(w, smallFile); err != nil { 138 t.Error(err) 139 } 140 smallSHA1 := fmt.Sprintf("%x", hash.Sum(nil)) 141 smallInfoMap := map[string]string{ 142 "one": "1", 143 "two": "2", 144 } 145 file, err := ue.UploadFile(ctx, buf, buf.Len(), smallFileName, "application/octet-stream", smallSHA1, smallInfoMap) 146 if err != nil { 147 t.Fatal(err) 148 } 149 150 defer func() { 151 // b2_delete_file_version 152 if err := file.DeleteFileVersion(ctx); err != nil { 153 t.Error(err) 154 } 155 }() 156 157 // b2_start_large_file 158 largeInfoMap := map[string]string{ 159 "one_billion": "1e9", 160 "two_trillion": "2eSomething, I guess 2e12", 161 } 162 lf, err := bucket.StartLargeFile(ctx, largeFileName, "application/octet-stream", largeInfoMap) 163 if err != nil { 164 t.Fatal(err) 165 } 166 167 // b2_get_upload_part_url 168 fc, err := lf.GetUploadPartURL(ctx) 169 if err != nil { 170 t.Fatal(err) 171 } 172 173 // b2_upload_part 174 largeFile := io.LimitReader(zReader{}, 10e6) // 10M 175 for i := 0; i < 2; i++ { 176 r := io.LimitReader(largeFile, 5e6) // 5M 177 hash := sha1.New() 178 buf := &bytes.Buffer{} 179 w := io.MultiWriter(hash, buf) 180 if _, err := io.Copy(w, r); err != nil { 181 t.Error(err) 182 } 183 if _, err := fc.UploadPart(ctx, buf, fmt.Sprintf("%x", hash.Sum(nil)), buf.Len(), i+1); err != nil { 184 t.Error(err) 185 } 186 } 187 188 // b2_finish_large_file 189 lfile, err := lf.FinishLargeFile(ctx) 190 if err != nil { 191 t.Fatal(err) 192 } 193 194 // b2_get_file_info 195 smallInfo, err := file.GetFileInfo(ctx) 196 if err != nil { 197 t.Fatal(err) 198 } 199 compareFileAndInfo(t, smallInfo, smallFileName, smallSHA1, smallInfoMap) 200 largeInfo, err := lfile.GetFileInfo(ctx) 201 if err != nil { 202 t.Fatal(err) 203 } 204 compareFileAndInfo(t, largeInfo, largeFileName, "none", largeInfoMap) 205 206 defer func() { 207 if err := lfile.DeleteFileVersion(ctx); err != nil { 208 t.Error(err) 209 } 210 }() 211 212 clf, err := bucket.StartLargeFile(ctx, largeFileName, "application/octet-stream", nil) 213 if err != nil { 214 t.Fatal(err) 215 } 216 217 // b2_cancel_large_file 218 if err := clf.CancelLargeFile(ctx); err != nil { 219 t.Fatal(err) 220 } 221 222 // b2_list_file_names 223 files, _, err := bucket.ListFileNames(ctx, 100, "", "", "") 224 if err != nil { 225 t.Fatal(err) 226 } 227 if len(files) != 2 { 228 t.Errorf("expected 2 files, got %d: %v", len(files), files) 229 } 230 231 // b2_download_file_by_name 232 fr, err := bucket.DownloadFileByName(ctx, smallFileName, 0, 0) 233 if err != nil { 234 t.Fatal(err) 235 } 236 if fr.SHA1 != smallSHA1 { 237 t.Errorf("small file SHAs don't match: got %q, want %q", fr.SHA1, smallSHA1) 238 } 239 lbuf := &bytes.Buffer{} 240 if _, err := io.Copy(lbuf, fr); err != nil { 241 t.Fatal(err) 242 } 243 if lbuf.Len() != fr.ContentLength { 244 t.Errorf("small file retreived lengths don't match: got %d, want %d", lbuf.Len(), fr.ContentLength) 245 } 246 247 // b2_hide_file 248 hf, err := bucket.HideFile(ctx, smallFileName) 249 if err != nil { 250 t.Fatal(err) 251 } 252 defer func() { 253 if err := hf.DeleteFileVersion(ctx); err != nil { 254 t.Error(err) 255 } 256 }() 257 258 // b2_list_file_versions 259 files, _, _, err = bucket.ListFileVersions(ctx, 100, "", "", "", "") 260 if err != nil { 261 t.Fatal(err) 262 } 263 if len(files) != 3 { 264 t.Errorf("expected 3 files, got %d: %v", len(files), files) 265 } 266 267 // b2_get_download_authorization 268 if _, err := bucket.GetDownloadAuthorization(ctx, "foo/", 24*time.Hour, "attachment"); err != nil { 269 t.Errorf("failed to get download auth token: %v", err) 270 } 271} 272 273func TestUploadAuthAfterConnectionHang(t *testing.T) { 274 id := os.Getenv(apiID) 275 key := os.Getenv(apiKey) 276 if id == "" || key == "" { 277 t.Skipf("B2_ACCOUNT_ID or B2_SECRET_KEY unset; skipping integration tests") 278 } 279 ctx := context.Background() 280 281 hung := make(chan struct{}) 282 283 // An http.RoundTripper that dies and hangs after sending ~10k bytes. 284 hang := func() { 285 close(hung) 286 select {} 287 } 288 tport := transport.WithFailures(nil, transport.AfterNBytes(10000, hang)) 289 290 b2, err := AuthorizeAccount(ctx, id, key, Transport(tport)) 291 if err != nil { 292 t.Fatal(err) 293 } 294 bname := id + "-" + bucketName 295 bucket, err := b2.CreateBucket(ctx, bname, "", nil, nil) 296 if err != nil { 297 t.Fatal(err) 298 } 299 defer func() { 300 if err := bucket.DeleteBucket(ctx); err != nil { 301 t.Error(err) 302 } 303 }() 304 ue, err := bucket.GetUploadURL(ctx) 305 if err != nil { 306 t.Fatal(err) 307 } 308 309 smallFile := io.LimitReader(zReader{}, 1024*50) // 50k 310 hash := sha1.New() 311 buf := &bytes.Buffer{} 312 w := io.MultiWriter(hash, buf) 313 if _, err := io.Copy(w, smallFile); err != nil { 314 t.Error(err) 315 } 316 smallSHA1 := fmt.Sprintf("%x", hash.Sum(nil)) 317 318 go func() { 319 ue.UploadFile(ctx, buf, buf.Len(), smallFileName, "application/octet-stream", smallSHA1, nil) 320 }() 321 322 <-hung 323 324 // Do the whole thing again with the same upload auth, before the remote end 325 // notices we're gone. 326 smallFile = io.LimitReader(zReader{}, 1024*50) // 50k again 327 buf.Reset() 328 if _, err := io.Copy(buf, smallFile); err != nil { 329 t.Error(err) 330 } 331 file, err := ue.UploadFile(ctx, buf, buf.Len(), smallFileName, "application/octet-stream", smallSHA1, nil) 332 if err == nil { 333 t.Error("expected an error, got none") 334 if err := file.DeleteFileVersion(ctx); err != nil { 335 t.Error(err) 336 } 337 } 338 if Action(err) != AttemptNewUpload { 339 t.Errorf("Action(%v): got %v, want AttemptNewUpload", err, Action(err)) 340 } 341} 342 343func TestCancelledContextCancelsHTTPRequest(t *testing.T) { 344 id := os.Getenv(apiID) 345 key := os.Getenv(apiKey) 346 if id == "" || key == "" { 347 t.Skipf("B2_ACCOUNT_ID or B2_SECRET_KEY unset; skipping integration tests") 348 } 349 ctx := context.Background() 350 351 tport := transport.WithFailures(nil, transport.MatchPathSubstring("b2_upload_file"), transport.FailureRate(1), transport.Stall(2*time.Second)) 352 353 b2, err := AuthorizeAccount(ctx, id, key, Transport(tport)) 354 if err != nil { 355 t.Fatal(err) 356 } 357 bname := id + "-" + bucketName 358 bucket, err := b2.CreateBucket(ctx, bname, "", nil, nil) 359 if err != nil { 360 t.Fatal(err) 361 } 362 defer func() { 363 if err := bucket.DeleteBucket(ctx); err != nil { 364 t.Error(err) 365 } 366 }() 367 ue, err := bucket.GetUploadURL(ctx) 368 if err != nil { 369 t.Fatal(err) 370 } 371 372 smallFile := io.LimitReader(zReader{}, 1024*50) // 50k 373 hash := sha1.New() 374 buf := &bytes.Buffer{} 375 w := io.MultiWriter(hash, buf) 376 if _, err := io.Copy(w, smallFile); err != nil { 377 t.Error(err) 378 } 379 smallSHA1 := fmt.Sprintf("%x", hash.Sum(nil)) 380 cctx, cancel := context.WithCancel(ctx) 381 go func() { 382 time.Sleep(1) 383 cancel() 384 }() 385 if _, err := ue.UploadFile(cctx, buf, buf.Len(), smallFileName, "application/octet-stream", smallSHA1, nil); err != context.Canceled { 386 t.Errorf("expected canceled context, but got %v", err) 387 } 388} 389 390func TestDeadlineExceededContextCancelsHTTPRequest(t *testing.T) { 391 id := os.Getenv(apiID) 392 key := os.Getenv(apiKey) 393 if id == "" || key == "" { 394 t.Skipf("B2_ACCOUNT_ID or B2_SECRET_KEY unset; skipping integration tests") 395 } 396 ctx := context.Background() 397 398 tport := transport.WithFailures(nil, transport.MatchPathSubstring("b2_upload_file"), transport.FailureRate(1), transport.Stall(2*time.Second)) 399 b2, err := AuthorizeAccount(ctx, id, key, Transport(tport)) 400 if err != nil { 401 t.Fatal(err) 402 } 403 bname := id + "-" + bucketName 404 bucket, err := b2.CreateBucket(ctx, bname, "", nil, nil) 405 if err != nil { 406 t.Fatal(err) 407 } 408 defer func() { 409 if err := bucket.DeleteBucket(ctx); err != nil { 410 t.Error(err) 411 } 412 }() 413 ue, err := bucket.GetUploadURL(ctx) 414 if err != nil { 415 t.Fatal(err) 416 } 417 418 smallFile := io.LimitReader(zReader{}, 1024*50) // 50k 419 hash := sha1.New() 420 buf := &bytes.Buffer{} 421 w := io.MultiWriter(hash, buf) 422 if _, err := io.Copy(w, smallFile); err != nil { 423 t.Error(err) 424 } 425 smallSHA1 := fmt.Sprintf("%x", hash.Sum(nil)) 426 cctx, cancel := context.WithTimeout(ctx, time.Second) 427 defer cancel() 428 if _, err := ue.UploadFile(cctx, buf, buf.Len(), smallFileName, "application/octet-stream", smallSHA1, nil); err != context.DeadlineExceeded { 429 t.Errorf("expected deadline exceeded error, but got %v", err) 430 } 431} 432 433func compareFileAndInfo(t *testing.T, info *FileInfo, name, sha1 string, imap map[string]string) { 434 if info.Name != name { 435 t.Errorf("got %q, want %q", info.Name, name) 436 } 437 if info.SHA1 != sha1 { 438 t.Errorf("got %q, want %q", info.SHA1, sha1) 439 } 440 if !reflect.DeepEqual(info.Info, imap) { 441 t.Errorf("got %v, want %v", info.Info, imap) 442 } 443} 444 445// from https://www.backblaze.com/b2/docs/string_encoding.html 446var testCases = `[ 447 {"fullyEncoded": "%20", "minimallyEncoded": "+", "string": " "}, 448 {"fullyEncoded": "%21", "minimallyEncoded": "!", "string": "!"}, 449 {"fullyEncoded": "%22", "minimallyEncoded": "%22", "string": "\""}, 450 {"fullyEncoded": "%23", "minimallyEncoded": "%23", "string": "#"}, 451 {"fullyEncoded": "%24", "minimallyEncoded": "$", "string": "$"}, 452 {"fullyEncoded": "%25", "minimallyEncoded": "%25", "string": "%"}, 453 {"fullyEncoded": "%26", "minimallyEncoded": "%26", "string": "&"}, 454 {"fullyEncoded": "%27", "minimallyEncoded": "'", "string": "'"}, 455 {"fullyEncoded": "%28", "minimallyEncoded": "(", "string": "("}, 456 {"fullyEncoded": "%29", "minimallyEncoded": ")", "string": ")"}, 457 {"fullyEncoded": "%2A", "minimallyEncoded": "*", "string": "*"}, 458 {"fullyEncoded": "%2B", "minimallyEncoded": "%2B", "string": "+"}, 459 {"fullyEncoded": "%2C", "minimallyEncoded": "%2C", "string": ","}, 460 {"fullyEncoded": "%2D", "minimallyEncoded": "-", "string": "-"}, 461 {"fullyEncoded": "%2E", "minimallyEncoded": ".", "string": "."}, 462 {"fullyEncoded": "/", "minimallyEncoded": "/", "string": "/"}, 463 {"fullyEncoded": "%30", "minimallyEncoded": "0", "string": "0"}, 464 {"fullyEncoded": "%31", "minimallyEncoded": "1", "string": "1"}, 465 {"fullyEncoded": "%32", "minimallyEncoded": "2", "string": "2"}, 466 {"fullyEncoded": "%33", "minimallyEncoded": "3", "string": "3"}, 467 {"fullyEncoded": "%34", "minimallyEncoded": "4", "string": "4"}, 468 {"fullyEncoded": "%35", "minimallyEncoded": "5", "string": "5"}, 469 {"fullyEncoded": "%36", "minimallyEncoded": "6", "string": "6"}, 470 {"fullyEncoded": "%37", "minimallyEncoded": "7", "string": "7"}, 471 {"fullyEncoded": "%38", "minimallyEncoded": "8", "string": "8"}, 472 {"fullyEncoded": "%39", "minimallyEncoded": "9", "string": "9"}, 473 {"fullyEncoded": "%3A", "minimallyEncoded": ":", "string": ":"}, 474 {"fullyEncoded": "%3B", "minimallyEncoded": ";", "string": ";"}, 475 {"fullyEncoded": "%3C", "minimallyEncoded": "%3C", "string": "<"}, 476 {"fullyEncoded": "%3D", "minimallyEncoded": "=", "string": "="}, 477 {"fullyEncoded": "%3E", "minimallyEncoded": "%3E", "string": ">"}, 478 {"fullyEncoded": "%3F", "minimallyEncoded": "%3F", "string": "?"}, 479 {"fullyEncoded": "%40", "minimallyEncoded": "@", "string": "@"}, 480 {"fullyEncoded": "%41", "minimallyEncoded": "A", "string": "A"}, 481 {"fullyEncoded": "%42", "minimallyEncoded": "B", "string": "B"}, 482 {"fullyEncoded": "%43", "minimallyEncoded": "C", "string": "C"}, 483 {"fullyEncoded": "%44", "minimallyEncoded": "D", "string": "D"}, 484 {"fullyEncoded": "%45", "minimallyEncoded": "E", "string": "E"}, 485 {"fullyEncoded": "%46", "minimallyEncoded": "F", "string": "F"}, 486 {"fullyEncoded": "%47", "minimallyEncoded": "G", "string": "G"}, 487 {"fullyEncoded": "%48", "minimallyEncoded": "H", "string": "H"}, 488 {"fullyEncoded": "%49", "minimallyEncoded": "I", "string": "I"}, 489 {"fullyEncoded": "%4A", "minimallyEncoded": "J", "string": "J"}, 490 {"fullyEncoded": "%4B", "minimallyEncoded": "K", "string": "K"}, 491 {"fullyEncoded": "%4C", "minimallyEncoded": "L", "string": "L"}, 492 {"fullyEncoded": "%4D", "minimallyEncoded": "M", "string": "M"}, 493 {"fullyEncoded": "%4E", "minimallyEncoded": "N", "string": "N"}, 494 {"fullyEncoded": "%4F", "minimallyEncoded": "O", "string": "O"}, 495 {"fullyEncoded": "%50", "minimallyEncoded": "P", "string": "P"}, 496 {"fullyEncoded": "%51", "minimallyEncoded": "Q", "string": "Q"}, 497 {"fullyEncoded": "%52", "minimallyEncoded": "R", "string": "R"}, 498 {"fullyEncoded": "%53", "minimallyEncoded": "S", "string": "S"}, 499 {"fullyEncoded": "%54", "minimallyEncoded": "T", "string": "T"}, 500 {"fullyEncoded": "%55", "minimallyEncoded": "U", "string": "U"}, 501 {"fullyEncoded": "%56", "minimallyEncoded": "V", "string": "V"}, 502 {"fullyEncoded": "%57", "minimallyEncoded": "W", "string": "W"}, 503 {"fullyEncoded": "%58", "minimallyEncoded": "X", "string": "X"}, 504 {"fullyEncoded": "%59", "minimallyEncoded": "Y", "string": "Y"}, 505 {"fullyEncoded": "%5A", "minimallyEncoded": "Z", "string": "Z"}, 506 {"fullyEncoded": "%5B", "minimallyEncoded": "%5B", "string": "["}, 507 {"fullyEncoded": "%5C", "minimallyEncoded": "%5C", "string": "\\"}, 508 {"fullyEncoded": "%5D", "minimallyEncoded": "%5D", "string": "]"}, 509 {"fullyEncoded": "%5E", "minimallyEncoded": "%5E", "string": "^"}, 510 {"fullyEncoded": "%5F", "minimallyEncoded": "_", "string": "_"}, 511 {"fullyEncoded": "%60", "minimallyEncoded": "%60", "string": "` + "`" + `"}, 512 {"fullyEncoded": "%61", "minimallyEncoded": "a", "string": "a"}, 513 {"fullyEncoded": "%62", "minimallyEncoded": "b", "string": "b"}, 514 {"fullyEncoded": "%63", "minimallyEncoded": "c", "string": "c"}, 515 {"fullyEncoded": "%64", "minimallyEncoded": "d", "string": "d"}, 516 {"fullyEncoded": "%65", "minimallyEncoded": "e", "string": "e"}, 517 {"fullyEncoded": "%66", "minimallyEncoded": "f", "string": "f"}, 518 {"fullyEncoded": "%67", "minimallyEncoded": "g", "string": "g"}, 519 {"fullyEncoded": "%68", "minimallyEncoded": "h", "string": "h"}, 520 {"fullyEncoded": "%69", "minimallyEncoded": "i", "string": "i"}, 521 {"fullyEncoded": "%6A", "minimallyEncoded": "j", "string": "j"}, 522 {"fullyEncoded": "%6B", "minimallyEncoded": "k", "string": "k"}, 523 {"fullyEncoded": "%6C", "minimallyEncoded": "l", "string": "l"}, 524 {"fullyEncoded": "%6D", "minimallyEncoded": "m", "string": "m"}, 525 {"fullyEncoded": "%6E", "minimallyEncoded": "n", "string": "n"}, 526 {"fullyEncoded": "%6F", "minimallyEncoded": "o", "string": "o"}, 527 {"fullyEncoded": "%70", "minimallyEncoded": "p", "string": "p"}, 528 {"fullyEncoded": "%71", "minimallyEncoded": "q", "string": "q"}, 529 {"fullyEncoded": "%72", "minimallyEncoded": "r", "string": "r"}, 530 {"fullyEncoded": "%73", "minimallyEncoded": "s", "string": "s"}, 531 {"fullyEncoded": "%74", "minimallyEncoded": "t", "string": "t"}, 532 {"fullyEncoded": "%75", "minimallyEncoded": "u", "string": "u"}, 533 {"fullyEncoded": "%76", "minimallyEncoded": "v", "string": "v"}, 534 {"fullyEncoded": "%77", "minimallyEncoded": "w", "string": "w"}, 535 {"fullyEncoded": "%78", "minimallyEncoded": "x", "string": "x"}, 536 {"fullyEncoded": "%79", "minimallyEncoded": "y", "string": "y"}, 537 {"fullyEncoded": "%7A", "minimallyEncoded": "z", "string": "z"}, 538 {"fullyEncoded": "%7B", "minimallyEncoded": "%7B", "string": "{"}, 539 {"fullyEncoded": "%7C", "minimallyEncoded": "%7C", "string": "|"}, 540 {"fullyEncoded": "%7D", "minimallyEncoded": "%7D", "string": "}"}, 541 {"fullyEncoded": "%7E", "minimallyEncoded": "~", "string": "~"}, 542 {"fullyEncoded": "%7F", "minimallyEncoded": "%7F", "string": "\u007f"}, 543 {"fullyEncoded": "%E8%87%AA%E7%94%B1", "minimallyEncoded": "%E8%87%AA%E7%94%B1", "string": "\u81ea\u7531"}, 544 {"fullyEncoded": "%F0%90%90%80", "minimallyEncoded": "%F0%90%90%80", "string": "\ud801\udc00"} 545]` 546 547type testCase struct { 548 Full string `json:"fullyEncoded"` 549 Min string `json:"minimallyEncoded"` 550 Raw string `json:"string"` 551} 552 553func TestEscapes(t *testing.T) { 554 dec := json.NewDecoder(strings.NewReader(testCases)) 555 var tcs []testCase 556 if err := dec.Decode(&tcs); err != nil { 557 t.Fatal(err) 558 } 559 for _, tc := range tcs { 560 en := escape(tc.Raw) 561 if !(en == tc.Full || en == tc.Min) { 562 t.Errorf("encode %q: got %q, want %q or %q", tc.Raw, en, tc.Min, tc.Full) 563 } 564 565 m, err := unescape(tc.Min) 566 if err != nil { 567 t.Errorf("decode %q: %v", tc.Min, err) 568 } 569 if m != tc.Raw { 570 t.Errorf("decode %q: got %q, want %q", tc.Min, m, tc.Raw) 571 } 572 f, err := unescape(tc.Full) 573 if err != nil { 574 t.Errorf("decode %q: %v", tc.Full, err) 575 } 576 if f != tc.Raw { 577 t.Errorf("decode %q: got %q, want %q", tc.Full, f, tc.Raw) 578 } 579 } 580} 581 582func TestUploadDownloadFilenameEscaping(t *testing.T) { 583 filename := "file%foo.txt" 584 585 id := os.Getenv(apiID) 586 key := os.Getenv(apiKey) 587 588 if id == "" || key == "" { 589 t.Skipf("B2_ACCOUNT_ID or B2_SECRET_KEY unset; skipping integration tests") 590 } 591 ctx := context.Background() 592 593 // b2_authorize_account 594 b2, err := AuthorizeAccount(ctx, id, key, UserAgent("blazer-base-test")) 595 if err != nil { 596 t.Fatal(err) 597 } 598 599 // b2_create_bucket 600 bname := id + "-" + bucketName 601 bucket, err := b2.CreateBucket(ctx, bname, "", nil, nil) 602 if err != nil { 603 t.Fatal(err) 604 } 605 606 defer func() { 607 // b2_delete_bucket 608 if err := bucket.DeleteBucket(ctx); err != nil { 609 t.Error(err) 610 } 611 }() 612 613 // b2_get_upload_url 614 ue, err := bucket.GetUploadURL(ctx) 615 if err != nil { 616 t.Fatal(err) 617 } 618 619 // b2_upload_file 620 smallFile := io.LimitReader(zReader{}, 128) 621 hash := sha1.New() 622 buf := &bytes.Buffer{} 623 w := io.MultiWriter(hash, buf) 624 if _, err := io.Copy(w, smallFile); err != nil { 625 t.Error(err) 626 } 627 smallSHA1 := fmt.Sprintf("%x", hash.Sum(nil)) 628 file, err := ue.UploadFile(ctx, buf, buf.Len(), filename, "application/octet-stream", smallSHA1, nil) 629 if err != nil { 630 t.Fatal(err) 631 } 632 633 defer func() { 634 // b2_delete_file_version 635 if err := file.DeleteFileVersion(ctx); err != nil { 636 t.Error(err) 637 } 638 }() 639 640 // b2_download_file_by_name 641 fr, err := bucket.DownloadFileByName(ctx, filename, 0, 0) 642 if err != nil { 643 t.Fatal(err) 644 } 645 lbuf := &bytes.Buffer{} 646 if _, err := io.Copy(lbuf, fr); err != nil { 647 t.Fatal(err) 648 } 649} 650