1// Copyright 2015 Google LLC
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package gensupport
6
7import (
8	"bytes"
9	cryptorand "crypto/rand"
10	"io"
11	"io/ioutil"
12	mathrand "math/rand"
13	"net/http"
14	"reflect"
15	"strings"
16	"testing"
17
18	"google.golang.org/api/googleapi"
19)
20
21func TestContentSniffing(t *testing.T) {
22	type testCase struct {
23		data     []byte // the data to read from the Reader
24		finalErr error  // error to return after data has been read
25
26		wantContentType       string
27		wantContentTypeResult bool
28	}
29
30	for _, tc := range []testCase{
31		{
32			data:                  []byte{0, 0, 0, 0},
33			finalErr:              nil,
34			wantContentType:       "application/octet-stream",
35			wantContentTypeResult: true,
36		},
37		{
38			data:                  []byte(""),
39			finalErr:              nil,
40			wantContentType:       "text/plain; charset=utf-8",
41			wantContentTypeResult: true,
42		},
43		{
44			data:                  []byte(""),
45			finalErr:              io.ErrUnexpectedEOF,
46			wantContentType:       "text/plain; charset=utf-8",
47			wantContentTypeResult: false,
48		},
49		{
50			data:                  []byte("abc"),
51			finalErr:              nil,
52			wantContentType:       "text/plain; charset=utf-8",
53			wantContentTypeResult: true,
54		},
55		{
56			data:                  []byte("abc"),
57			finalErr:              io.ErrUnexpectedEOF,
58			wantContentType:       "text/plain; charset=utf-8",
59			wantContentTypeResult: false,
60		},
61		// The following examples contain more bytes than are buffered for sniffing.
62		{
63			data:                  bytes.Repeat([]byte("a"), 513),
64			finalErr:              nil,
65			wantContentType:       "text/plain; charset=utf-8",
66			wantContentTypeResult: true,
67		},
68		{
69			data:                  bytes.Repeat([]byte("a"), 513),
70			finalErr:              io.ErrUnexpectedEOF,
71			wantContentType:       "text/plain; charset=utf-8",
72			wantContentTypeResult: true, // true because error is after first 512 bytes.
73		},
74	} {
75		er := &errReader{buf: tc.data, err: tc.finalErr}
76
77		sct := newContentSniffer(er)
78
79		// Even if was an error during the first 512 bytes, we should still be able to read those bytes.
80		buf, err := ioutil.ReadAll(sct)
81
82		if !reflect.DeepEqual(buf, tc.data) {
83			t.Fatalf("Failed reading buffer: got: %q; want:%q", buf, tc.data)
84		}
85
86		if err != tc.finalErr {
87			t.Fatalf("Reading buffer error: got: %v; want: %v", err, tc.finalErr)
88		}
89
90		ct, ok := sct.ContentType()
91		if ok != tc.wantContentTypeResult {
92			t.Fatalf("Content type result got: %v; want: %v", ok, tc.wantContentTypeResult)
93		}
94		if ok && ct != tc.wantContentType {
95			t.Fatalf("Content type got: %q; want: %q", ct, tc.wantContentType)
96		}
97	}
98}
99
100type staticContentTyper struct {
101	io.Reader
102}
103
104func (sct staticContentTyper) ContentType() string {
105	return "static content type"
106}
107
108func TestDetermineContentType(t *testing.T) {
109	data := []byte("abc")
110	rdr := func() io.Reader {
111		return bytes.NewBuffer(data)
112	}
113
114	type testCase struct {
115		r                  io.Reader
116		explicitConentType string
117		wantContentType    string
118	}
119
120	for _, tc := range []testCase{
121		{
122			r:               rdr(),
123			wantContentType: "text/plain; charset=utf-8",
124		},
125		{
126			r:               staticContentTyper{rdr()},
127			wantContentType: "static content type",
128		},
129		{
130			r:                  staticContentTyper{rdr()},
131			explicitConentType: "explicit",
132			wantContentType:    "explicit",
133		},
134	} {
135		r, ctype := DetermineContentType(tc.r, tc.explicitConentType)
136		got, err := ioutil.ReadAll(r)
137		if err != nil {
138			t.Fatalf("Failed reading buffer: %v", err)
139		}
140		if !reflect.DeepEqual(got, data) {
141			t.Fatalf("Failed reading buffer: got: %q; want:%q", got, data)
142		}
143
144		if ctype != tc.wantContentType {
145			t.Fatalf("Content type got: %q; want: %q", ctype, tc.wantContentType)
146		}
147	}
148}
149
150func TestNewInfoFromMedia(t *testing.T) {
151	const textType = "text/plain; charset=utf-8"
152	for _, test := range []struct {
153		desc                                   string
154		r                                      io.Reader
155		opts                                   []googleapi.MediaOption
156		wantType                               string
157		wantMedia, wantBuffer, wantSingleChunk bool
158	}{
159		{
160			desc:            "an empty reader results in a MediaBuffer with a single, empty chunk",
161			r:               new(bytes.Buffer),
162			opts:            nil,
163			wantType:        textType,
164			wantBuffer:      true,
165			wantSingleChunk: true,
166		},
167		{
168			desc:            "ContentType is observed",
169			r:               new(bytes.Buffer),
170			opts:            []googleapi.MediaOption{googleapi.ContentType("xyz")},
171			wantType:        "xyz",
172			wantBuffer:      true,
173			wantSingleChunk: true,
174		},
175		{
176			desc:            "chunk size of zero: don't use a MediaBuffer; upload as a single chunk",
177			r:               strings.NewReader("12345"),
178			opts:            []googleapi.MediaOption{googleapi.ChunkSize(0)},
179			wantType:        textType,
180			wantMedia:       true,
181			wantSingleChunk: true,
182		},
183		{
184			desc:            "chunk size > data size: MediaBuffer with single chunk",
185			r:               strings.NewReader("12345"),
186			opts:            []googleapi.MediaOption{googleapi.ChunkSize(100)},
187			wantType:        textType,
188			wantBuffer:      true,
189			wantSingleChunk: true,
190		},
191		{
192			desc:            "chunk size == data size: MediaBuffer with single chunk",
193			r:               &nullReader{googleapi.MinUploadChunkSize},
194			opts:            []googleapi.MediaOption{googleapi.ChunkSize(1)},
195			wantType:        "application/octet-stream",
196			wantBuffer:      true,
197			wantSingleChunk: true,
198		},
199		{
200			desc: "chunk size < data size: MediaBuffer, not single chunk",
201			// Note that ChunkSize = 1 is rounded up to googleapi.MinUploadChunkSize.
202			r:               &nullReader{2 * googleapi.MinUploadChunkSize},
203			opts:            []googleapi.MediaOption{googleapi.ChunkSize(1)},
204			wantType:        "application/octet-stream",
205			wantBuffer:      true,
206			wantSingleChunk: false,
207		},
208	} {
209
210		mi := NewInfoFromMedia(test.r, test.opts)
211		if got, want := mi.mType, test.wantType; got != want {
212			t.Errorf("%s: type: got %q, want %q", test.desc, got, want)
213		}
214		if got, want := (mi.media != nil), test.wantMedia; got != want {
215			t.Errorf("%s: media non-nil: got %t, want %t", test.desc, got, want)
216		}
217		if got, want := (mi.buffer != nil), test.wantBuffer; got != want {
218			t.Errorf("%s: buffer non-nil: got %t, want %t", test.desc, got, want)
219		}
220		if got, want := mi.singleChunk, test.wantSingleChunk; got != want {
221			t.Errorf("%s: singleChunk: got %t, want %t", test.desc, got, want)
222		}
223	}
224}
225
226func TestUploadRequest(t *testing.T) {
227	for _, test := range []struct {
228		desc            string
229		r               io.Reader
230		chunkSize       int
231		wantContentType string
232		wantUploadType  string
233	}{
234		{
235			desc:            "chunk size of zero: don't use a MediaBuffer; upload as a single chunk",
236			r:               strings.NewReader("12345"),
237			chunkSize:       0,
238			wantContentType: "multipart/related;",
239		},
240		{
241			desc:            "chunk size > data size: MediaBuffer with single chunk",
242			r:               strings.NewReader("12345"),
243			chunkSize:       100,
244			wantContentType: "multipart/related;",
245		},
246		{
247			desc:            "chunk size == data size: MediaBuffer with single chunk",
248			r:               &nullReader{googleapi.MinUploadChunkSize},
249			chunkSize:       1,
250			wantContentType: "multipart/related;",
251		},
252		{
253			desc: "chunk size < data size: MediaBuffer, not single chunk",
254			// Note that ChunkSize = 1 is rounded up to googleapi.MinUploadChunkSize.
255			r:              &nullReader{2 * googleapi.MinUploadChunkSize},
256			chunkSize:      1,
257			wantUploadType: "application/octet-stream",
258		},
259	} {
260		mi := NewInfoFromMedia(test.r, []googleapi.MediaOption{googleapi.ChunkSize(test.chunkSize)})
261		h := http.Header{}
262		mi.UploadRequest(h, new(bytes.Buffer))
263		if got, want := h.Get("Content-Type"), test.wantContentType; !strings.HasPrefix(got, want) {
264			t.Errorf("%s: Content-Type: got %q, want prefix %q", test.desc, got, want)
265		}
266		if got, want := h.Get("X-Upload-Content-Type"), test.wantUploadType; got != want {
267			t.Errorf("%s: X-Upload-Content-Type: got %q, want %q", test.desc, got, want)
268		}
269	}
270}
271
272func TestUploadRequestGetBody(t *testing.T) {
273	// Test that a single chunk results in a getBody function that is non-nil, and
274	// that produces the same content as the original body.
275
276	// Restore the crypto/rand.Reader mocked out below.
277	defer func(old io.Reader) { cryptorand.Reader = old }(cryptorand.Reader)
278
279	for i, test := range []struct {
280		desc        string
281		r           io.Reader
282		chunkSize   int
283		wantGetBody bool
284	}{
285		{
286			desc:        "chunk size of zero: no getBody",
287			r:           &nullReader{10},
288			chunkSize:   0,
289			wantGetBody: false,
290		},
291		{
292			desc:        "chunk size == data size: 1 chunk, getBody",
293			r:           &nullReader{googleapi.MinUploadChunkSize},
294			chunkSize:   1,
295			wantGetBody: true,
296		},
297		{
298			desc: "chunk size < data size: MediaBuffer, >1 chunk, no getBody",
299			// No getBody here, because the initial request contains no media data
300			// Note that ChunkSize = 1 is rounded up to googleapi.MinUploadChunkSize.
301			r:           &nullReader{2 * googleapi.MinUploadChunkSize},
302			chunkSize:   1,
303			wantGetBody: false,
304		},
305	} {
306		cryptorand.Reader = mathrand.New(mathrand.NewSource(int64(i)))
307
308		mi := NewInfoFromMedia(test.r, []googleapi.MediaOption{googleapi.ChunkSize(test.chunkSize)})
309		r, getBody, _ := mi.UploadRequest(http.Header{}, bytes.NewBuffer([]byte("body")))
310		if got, want := (getBody != nil), test.wantGetBody; got != want {
311			t.Errorf("%s: getBody: got %t, want %t", test.desc, got, want)
312			continue
313		}
314		if getBody == nil {
315			continue
316		}
317		want, err := ioutil.ReadAll(r)
318		if err != nil {
319			t.Fatal(err)
320		}
321		for i := 0; i < 3; i++ {
322			rc, err := getBody()
323			if err != nil {
324				t.Fatal(err)
325			}
326			got, err := ioutil.ReadAll(rc)
327			if err != nil {
328				t.Fatal(err)
329			}
330			if !bytes.Equal(got, want) {
331				t.Errorf("%s, %d:\ngot:\n%s\nwant:\n%s", test.desc, i, string(got), string(want))
332			}
333		}
334	}
335}
336
337func TestResumableUpload(t *testing.T) {
338	for _, test := range []struct {
339		desc                string
340		r                   io.Reader
341		chunkSize           int
342		wantUploadType      string
343		wantResumableUpload bool
344	}{
345		{
346			desc:                "chunk size of zero: don't use a MediaBuffer; upload as a single chunk",
347			r:                   strings.NewReader("12345"),
348			chunkSize:           0,
349			wantUploadType:      "multipart",
350			wantResumableUpload: false,
351		},
352		{
353			desc:                "chunk size > data size: MediaBuffer with single chunk",
354			r:                   strings.NewReader("12345"),
355			chunkSize:           100,
356			wantUploadType:      "multipart",
357			wantResumableUpload: false,
358		},
359		{
360			desc: "chunk size == data size: MediaBuffer with single chunk",
361			// (Because nullReader returns EOF with the last bytes.)
362			r:                   &nullReader{googleapi.MinUploadChunkSize},
363			chunkSize:           googleapi.MinUploadChunkSize,
364			wantUploadType:      "multipart",
365			wantResumableUpload: false,
366		},
367		{
368			desc: "chunk size < data size: MediaBuffer, not single chunk",
369			// Note that ChunkSize = 1 is rounded up to googleapi.MinUploadChunkSize.
370			r:                   &nullReader{2 * googleapi.MinUploadChunkSize},
371			chunkSize:           1,
372			wantUploadType:      "resumable",
373			wantResumableUpload: true,
374		},
375	} {
376		mi := NewInfoFromMedia(test.r, []googleapi.MediaOption{googleapi.ChunkSize(test.chunkSize)})
377		if got, want := mi.UploadType(), test.wantUploadType; got != want {
378			t.Errorf("%s: upload type: got %q, want %q", test.desc, got, want)
379		}
380		if got, want := mi.ResumableUpload("") != nil, test.wantResumableUpload; got != want {
381			t.Errorf("%s: resumable upload non-nil: got %t, want %t", test.desc, got, want)
382		}
383	}
384}
385
386// A nullReader simulates reading a fixed number of bytes.
387type nullReader struct {
388	remain int
389}
390
391// Read doesn't touch buf, but it does reduce the amount of bytes remaining
392// by len(buf).
393func (r *nullReader) Read(buf []byte) (int, error) {
394	n := len(buf)
395	if r.remain < n {
396		n = r.remain
397	}
398	r.remain -= n
399	var err error
400	if r.remain == 0 {
401		err = io.EOF
402	}
403	return n, err
404}
405