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