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