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