Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(502)

Side by Side Diff: go/src/infra/tools/cipd/client_test.go

Issue 1129043003: cipd: Refactor client to make it more readable. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « go/src/infra/tools/cipd/client.go ('k') | go/src/infra/tools/cipd/clock.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package cipd
6
7 import (
8 "bytes"
9 "fmt"
10 "io"
11 "io/ioutil"
12 "net/http"
13 "net/http/httptest"
14 "net/url"
15 "os"
16 "path/filepath"
17 "testing"
18 "time"
19
20 . "github.com/smartystreets/goconvey/convey"
21
22 "infra/tools/cipd/common"
23 "infra/tools/cipd/local"
24 )
25
26 func TestUploadToCAS(t *testing.T) {
27 Convey("UploadToCAS full flow", t, func(c C) {
28 client := mockClient(c, []expectedHTTPCall{
29 {
30 Method: "POST",
31 Path: "/_ah/api/cas/v1/upload/SHA1/abc",
32 Reply: `{"status":"SUCCESS","upload_session_id" :"12345","upload_url":"http://localhost"}`,
33 },
34 {
35 Method: "POST",
36 Path: "/_ah/api/cas/v1/finalize/12345",
37 Reply: `{"status":"VERIFYING"}`,
38 },
39 {
40 Method: "POST",
41 Path: "/_ah/api/cas/v1/finalize/12345",
42 Reply: `{"status":"PUBLISHED"}`,
43 },
44 })
45 client.storage = &mockedStorage{c, nil}
46 err := client.UploadToCAS("abc", nil, nil)
47 So(err, ShouldBeNil)
48 })
49
50 Convey("UploadToCAS timeout", t, func(c C) {
51 // Append a bunch of "still verifying" responses at the end.
52 calls := []expectedHTTPCall{
53 {
54 Method: "POST",
55 Path: "/_ah/api/cas/v1/upload/SHA1/abc",
56 Reply: `{"status":"SUCCESS","upload_session_id" :"12345","upload_url":"http://localhost"}`,
57 },
58 }
59 for i := 0; i < 19; i++ {
60 calls = append(calls, expectedHTTPCall{
61 Method: "POST",
62 Path: "/_ah/api/cas/v1/finalize/12345",
63 Reply: `{"status":"VERIFYING"}`,
64 })
65 }
66 client := mockClient(c, calls)
67 client.storage = &mockedStorage{c, nil}
68 err := client.UploadToCAS("abc", nil, nil)
69 So(err, ShouldEqual, ErrFinalizationTimeout)
70 })
71 }
72
73 func TestRegisterInstance(t *testing.T) {
74 Convey("Mocking a package instance", t, func() {
75 // Build an empty package to be uploaded.
76 out := bytes.Buffer{}
77 err := local.BuildInstance(local.BuildInstanceOptions{
78 Input: []local.File{},
79 Output: &out,
80 PackageName: "testing",
81 })
82 So(err, ShouldBeNil)
83
84 // Open it for reading.
85 inst, err := local.OpenInstance(bytes.NewReader(out.Bytes()), "" )
86 So(err, ShouldBeNil)
87 Reset(func() { inst.Close() })
88
89 Convey("RegisterInstance full flow", func(c C) {
90 client := mockClient(c, []expectedHTTPCall{
91 {
92 Method: "POST",
93 Path: "/_ah/api/repo/v1/instance",
94 Query: url.Values{
95 "instance_id": []string{inst.Pi n().InstanceID},
96 "package_name": []string{inst.Pi n().PackageName},
97 },
98 Reply: `{
99 "status": "UPLOAD_FIRST",
100 "upload_session_id": "12345",
101 "upload_url": "http://localhost"
102 }`,
103 },
104 {
105 Method: "POST",
106 Path: "/_ah/api/cas/v1/finalize/12345" ,
107 Reply: `{"status":"PUBLISHED"}`,
108 },
109 {
110 Method: "POST",
111 Path: "/_ah/api/repo/v1/instance",
112 Query: url.Values{
113 "instance_id": []string{inst.Pi n().InstanceID},
114 "package_name": []string{inst.Pi n().PackageName},
115 },
116 Reply: `{
117 "status": "REGISTERED",
118 "instance": {
119 "registered_by": "user:a @example.com",
120 "registered_ts": "0"
121 }
122 }`,
123 },
124 })
125 client.storage = &mockedStorage{c, nil}
126 err = client.RegisterInstance(inst)
127 So(err, ShouldBeNil)
128 })
129
130 Convey("RegisterInstance already registered", func(c C) {
131 client := mockClient(c, []expectedHTTPCall{
132 {
133 Method: "POST",
134 Path: "/_ah/api/repo/v1/instance",
135 Query: url.Values{
136 "instance_id": []string{inst.Pi n().InstanceID},
137 "package_name": []string{inst.Pi n().PackageName},
138 },
139 Reply: `{
140 "status": "ALREADY_REGIS TERED",
141 "instance": {
142 "registered_by": "user:a@example.com",
143 "registered_ts": "0"
144 }
145 }`,
146 },
147 })
148 client.storage = &mockedStorage{c, nil}
149 err = client.RegisterInstance(inst)
150 So(err, ShouldBeNil)
151 })
152 })
153 }
154
155 func TestAttachTagsWhenReady(t *testing.T) {
156 Convey("AttachTagsWhenReady works", t, func(c C) {
157 client := mockClient(c, []expectedHTTPCall{
158 {
159 Method: "POST",
160 Path: "/_ah/api/repo/v1/tags",
161 Query: url.Values{
162 "instance_id": []string{"aaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaa"},
163 "package_name": []string{"pkgname"},
164 },
165 Body: `{"tags":["tag1:value1"]}`,
166 Reply: `{"status": "PROCESSING_NOT_FINISHED_YET" }`,
167 },
168 {
169 Method: "POST",
170 Path: "/_ah/api/repo/v1/tags",
171 Query: url.Values{
172 "instance_id": []string{"aaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaa"},
173 "package_name": []string{"pkgname"},
174 },
175 Body: `{"tags":["tag1:value1"]}`,
176 Reply: `{"status": "SUCCESS"}`,
177 },
178 })
179 pin := common.Pin{
180 PackageName: "pkgname",
181 InstanceID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
182 }
183 err := client.AttachTagsWhenReady(pin, []string{"tag1:value1"})
184 So(err, ShouldBeNil)
185 })
186
187 Convey("AttachTagsWhenReady timeout", t, func(c C) {
188 calls := []expectedHTTPCall{}
189 for i := 0; i < 12; i++ {
190 calls = append(calls, expectedHTTPCall{
191 Method: "POST",
192 Path: "/_ah/api/repo/v1/tags",
193 Query: url.Values{
194 "instance_id": []string{"aaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaa"},
195 "package_name": []string{"pkgname"},
196 },
197 Body: `{"tags":["tag1:value1"]}`,
198 Reply: `{"status": "PROCESSING_NOT_FINISHED_YET" }`,
199 })
200 }
201 client := mockClient(c, calls)
202 pin := common.Pin{
203 PackageName: "pkgname",
204 InstanceID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
205 }
206 err := client.AttachTagsWhenReady(pin, []string{"tag1:value1"})
207 So(err, ShouldEqual, ErrAttachTagsTimeout)
208 })
209 }
210
211 func TestFetch(t *testing.T) {
212 Convey("Mocking remote services", t, func() {
213 tempDir, err := ioutil.TempDir("", "cipd_test")
214 So(err, ShouldBeNil)
215 Reset(func() { os.RemoveAll(tempDir) })
216 tempFile := filepath.Join(tempDir, "pkg")
217
218 Convey("FetchInstance works", func(c C) {
219 inst := buildInstanceInMemory("pkgname", nil)
220 defer inst.Close()
221
222 out, err := os.OpenFile(tempFile, os.O_WRONLY|os.O_CREAT E, 0666)
223 So(err, ShouldBeNil)
224 closed := false
225 defer func() {
226 if !closed {
227 out.Close()
228 }
229 }()
230
231 client := mockClientForFetch(c, []local.PackageInstance{ inst})
232 err = client.FetchInstance(inst.Pin(), out)
233 So(err, ShouldBeNil)
234 out.Close()
235 closed = true
236
237 fetched, err := local.OpenInstanceFile(tempFile, "")
238 So(err, ShouldBeNil)
239 So(fetched.Pin(), ShouldResemble, inst.Pin())
240 })
241
242 Convey("FetchAndDeployInstance works", func(c C) {
243 // Build a package instance with some file.
244 inst := buildInstanceInMemory("testing/package", []local .File{
245 local.NewTestFile("file", "test data", false),
246 })
247 defer inst.Close()
248
249 // Install the package, fetching it from the fake server .
250 client := mockClientForFetch(c, []local.PackageInstance{ inst})
251 err = client.FetchAndDeployInstance(tempDir, inst.Pin())
252 So(err, ShouldBeNil)
253
254 // The file from the package should be installed.
255 data, err := ioutil.ReadFile(filepath.Join(tempDir, "fil e"))
256 So(err, ShouldBeNil)
257 So(data, ShouldResemble, []byte("test data"))
258 })
259 })
260 }
261
262 func TestProcessEnsureFile(t *testing.T) {
263 call := func(c C, data string) ([]common.Pin, error) {
264 client := mockClient(c, nil)
265 return client.ProcessEnsureFile(bytes.NewBufferString(data))
266 }
267
268 Convey("ProcessEnsureFile works", t, func(c C) {
269 out, err := call(c, `
270 # Comment
271
272 pkg/a 0000000000000000000000000000000000000000
273 pkg/b 1000000000000000000000000000000000000000
274 `)
275 So(err, ShouldBeNil)
276 So(out, ShouldResemble, []common.Pin{
277 {"pkg/a", "0000000000000000000000000000000000000000"},
278 {"pkg/b", "1000000000000000000000000000000000000000"},
279 })
280 })
281
282 Convey("ProcessEnsureFile empty", t, func(c C) {
283 out, err := call(c, "")
284 So(err, ShouldBeNil)
285 So(out, ShouldResemble, []common.Pin{})
286 })
287
288 Convey("ProcessEnsureFile bad package name", t, func(c C) {
289 _, err := call(c, "bad.package.name/a 0000000000000000000000000 000000000000000")
290 So(err, ShouldNotBeNil)
291 })
292
293 Convey("ProcessEnsureFile bad instance ID", t, func(c C) {
294 _, err := call(c, "pkg/a 0000")
295 So(err, ShouldNotBeNil)
296 })
297
298 Convey("ProcessEnsureFile bad line", t, func(c C) {
299 _, err := call(c, "pkg/a")
300 So(err, ShouldNotBeNil)
301 })
302 }
303
304 func TestEnsurePackages(t *testing.T) {
305 Convey("Mocking temp dir", t, func() {
306 tempDir, err := ioutil.TempDir("", "cipd_test")
307 So(err, ShouldBeNil)
308 Reset(func() { os.RemoveAll(tempDir) })
309
310 assertFile := func(relPath, data string) {
311 body, err := ioutil.ReadFile(filepath.Join(tempDir, relP ath))
312 So(err, ShouldBeNil)
313 So(string(body), ShouldEqual, data)
314 }
315
316 Convey("EnsurePackages full flow", func(c C) {
317 // Prepare a bunch of packages.
318 a1 := buildInstanceInMemory("pkg/a", []local.File{local. NewTestFile("file a 1", "test data", false)})
319 defer a1.Close()
320 a2 := buildInstanceInMemory("pkg/a", []local.File{local. NewTestFile("file a 2", "test data", false)})
321 defer a2.Close()
322 b := buildInstanceInMemory("pkg/b", []local.File{local.N ewTestFile("file b", "test data", false)})
323 defer b.Close()
324
325 // Calls EnsurePackages, mocking fetch backend first. Ba ckend will be mocked
326 // to serve only 'fetched' packages.
327 callEnsure := func(instances []local.PackageInstance, fe tched []local.PackageInstance) error {
328 client := mockClientForFetch(c, fetched)
329 pins := []common.Pin{}
330 for _, i := range instances {
331 pins = append(pins, i.Pin())
332 }
333 return client.EnsurePackages(tempDir, pins)
334 }
335
336 // Noop run on top of empty directory.
337 err := callEnsure(nil, nil)
338 So(err, ShouldBeNil)
339
340 // Specify same package twice. Fails.
341 err = callEnsure([]local.PackageInstance{a1, a2}, nil)
342 So(err, ShouldNotBeNil)
343
344 // Install a1 into a site root.
345 err = callEnsure([]local.PackageInstance{a1}, []local.Pa ckageInstance{a1})
346 So(err, ShouldBeNil)
347 assertFile("file a 1", "test data")
348 deployed, err := local.FindDeployed(tempDir)
349 So(err, ShouldBeNil)
350 So(deployed, ShouldResemble, []common.Pin{a1.Pin()})
351
352 // Noop run. Nothing is fetched.
353 err = callEnsure([]local.PackageInstance{a1}, nil)
354 So(err, ShouldBeNil)
355 assertFile("file a 1", "test data")
356 deployed, err = local.FindDeployed(tempDir)
357 So(err, ShouldBeNil)
358 So(deployed, ShouldResemble, []common.Pin{a1.Pin()})
359
360 // Upgrade a1 to a2.
361 err = callEnsure([]local.PackageInstance{a2}, []local.Pa ckageInstance{a2})
362 So(err, ShouldBeNil)
363 assertFile("file a 2", "test data")
364 deployed, err = local.FindDeployed(tempDir)
365 So(err, ShouldBeNil)
366 So(deployed, ShouldResemble, []common.Pin{a2.Pin()})
367
368 // Remove a2 and install b.
369 err = callEnsure([]local.PackageInstance{b}, []local.Pac kageInstance{b})
370 So(err, ShouldBeNil)
371 assertFile("file b", "test data")
372 deployed, err = local.FindDeployed(tempDir)
373 So(err, ShouldBeNil)
374 So(deployed, ShouldResemble, []common.Pin{b.Pin()})
375
376 // Remove b.
377 err = callEnsure(nil, nil)
378 So(err, ShouldBeNil)
379 deployed, err = local.FindDeployed(tempDir)
380 So(err, ShouldBeNil)
381 So(deployed, ShouldResemble, []common.Pin{})
382
383 // Install a1 and b.
384 err = callEnsure([]local.PackageInstance{a1, b}, []local .PackageInstance{a1, b})
385 So(err, ShouldBeNil)
386 assertFile("file a 1", "test data")
387 assertFile("file b", "test data")
388 deployed, err = local.FindDeployed(tempDir)
389 So(err, ShouldBeNil)
390 So(deployed, ShouldResemble, []common.Pin{a1.Pin(), b.Pi n()})
391 })
392 })
393 }
394
395 ////////////////////////////////////////////////////////////////////////////////
396
397 // buildInstanceInMemory makes fully functional PackageInstance object that uses
398 // memory buffer as a backing store.
399 func buildInstanceInMemory(pkgName string, files []local.File) local.PackageInst ance {
400 out := bytes.Buffer{}
401 err := local.BuildInstance(local.BuildInstanceOptions{
402 Input: files,
403 Output: &out,
404 PackageName: pkgName,
405 })
406 So(err, ShouldBeNil)
407 inst, err := local.OpenInstance(bytes.NewReader(out.Bytes()), "")
408 So(err, ShouldBeNil)
409 return inst
410 }
411
412 ////////////////////////////////////////////////////////////////////////////////
413
414 // mockClientForFetch returns Client with fetch related calls mocked.
415 func mockClientForFetch(c C, instances []local.PackageInstance) *Client {
416 // Mock RPC calls.
417 calls := []expectedHTTPCall{}
418 for _, inst := range instances {
419 calls = append(calls, expectedHTTPCall{
420 Method: "GET",
421 Path: "/_ah/api/repo/v1/instance",
422 Query: url.Values{
423 "instance_id": []string{inst.Pin().InstanceID},
424 "package_name": []string{inst.Pin().PackageName} ,
425 },
426 Reply: fmt.Sprintf(`{
427 "status": "SUCCESS",
428 "instance": {
429 "registered_by": "user:a@example.com",
430 "registered_ts": "0"
431 },
432 "fetch_url": "http://localhost/fetch/%s"
433 }`, inst.Pin().InstanceID),
434 })
435 }
436 client := mockClient(c, calls)
437
438 // Mock storage.
439 data := map[string][]byte{}
440 for _, inst := range instances {
441 r := inst.DataReader()
442 _, err := r.Seek(0, os.SEEK_SET)
443 c.So(err, ShouldBeNil)
444 blob, err := ioutil.ReadAll(r)
445 c.So(err, ShouldBeNil)
446 data["http://localhost/fetch/"+inst.Pin().InstanceID] = blob
447 }
448 client.storage = &mockedStorage{c, data}
449 return client
450 }
451
452 ////////////////////////////////////////////////////////////////////////////////
453
454 // mockedStorage implements storage by returning mocked data in 'download' and
455 // doing nothing in 'upload'.
456 type mockedStorage struct {
457 c C
458 data map[string][]byte
459 }
460
461 func (s *mockedStorage) download(url string, output io.WriteSeeker) error {
462 blob, ok := s.data[url]
463 if !ok {
464 return ErrDownloadError
465 }
466 _, err := output.Seek(0, os.SEEK_SET)
467 s.c.So(err, ShouldBeNil)
468 _, err = output.Write(blob)
469 s.c.So(err, ShouldBeNil)
470 return nil
471 }
472
473 func (s *mockedStorage) upload(url string, data io.ReadSeeker) error {
474 return nil
475 }
476
477 ////////////////////////////////////////////////////////////////////////////////
478
479 type mockedClocked struct {
480 ts time.Time
481 }
482
483 func (c *mockedClocked) now() time.Time { return c.ts }
484 func (c *mockedClocked) sleep(d time.Duration) { c.ts = c.ts.Add(d) }
485
486 ////////////////////////////////////////////////////////////////////////////////
487
488 type expectedHTTPCall struct {
489 Method string
490 Path string
491 Query url.Values
492 Body string
493 Headers http.Header
494 Reply string
495 Status int
496 ResponseHeaders http.Header
497 }
498
499 // mockClient returns Client with clock and HTTP calls mocked.
500 func mockClient(c C, expectations []expectedHTTPCall) *Client {
501 client := NewClient()
502 client.clock = &mockedClocked{}
503
504 // Kill factories. They should not be called for mocked client.
505 client.AuthenticatedClientFactory = nil
506 client.AnonymousClientFactory = nil
507
508 // Provide fake client instead.
509 handler := &expectedHTTPCallHandler{c, expectations, 0}
510 server := httptest.NewServer(handler)
511 Reset(func() {
512 server.Close()
513 // All expected calls should be made.
514 if handler.index != len(handler.calls) {
515 c.Printf("Unfinished calls: %v\n", handler.calls[handler .index:])
516 }
517 c.So(handler.index, ShouldEqual, len(handler.calls))
518 })
519 transport := &http.Transport{
520 Proxy: func(req *http.Request) (*url.URL, error) {
521 return url.Parse(server.URL)
522 },
523 }
524 client.ServiceURL = server.URL
525 client.anonClient = &http.Client{Transport: transport}
526 client.authClient = &http.Client{Transport: transport}
527
528 return client
529 }
530
531 // expectedHTTPCallHandler is http.Handler that serves mocked HTTP calls.
532 type expectedHTTPCallHandler struct {
533 c C
534 calls []expectedHTTPCall
535 index int
536 }
537
538 func (s *expectedHTTPCallHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque st) {
539 // Unexpected call?
540 if s.index == len(s.calls) {
541 s.c.Printf("Unexpected call: %v\n", r)
542 }
543 s.c.So(s.index, ShouldBeLessThan, len(s.calls))
544
545 // Fill in defaults.
546 exp := s.calls[s.index]
547 if exp.Method == "" {
548 exp.Method = "GET"
549 }
550 if exp.Query == nil {
551 exp.Query = url.Values{}
552 }
553 if exp.Headers == nil {
554 exp.Headers = http.Header{}
555 }
556
557 // Read body and essential headers.
558 body, err := ioutil.ReadAll(r.Body)
559 s.c.So(err, ShouldBeNil)
560 blacklist := map[string]bool{
561 "Accept-Encoding": true,
562 "Content-Length": true,
563 "Content-Type": true,
564 "User-Agent": true,
565 }
566 headers := http.Header{}
567 for k, v := range r.Header {
568 _, isExpected := exp.Headers[k]
569 if isExpected || !blacklist[k] {
570 headers[k] = v
571 }
572 }
573
574 // Check that request is what it is expected to be.
575 s.c.So(r.Method, ShouldEqual, exp.Method)
576 s.c.So(r.URL.Path, ShouldEqual, exp.Path)
577 s.c.So(r.URL.Query(), ShouldResemble, exp.Query)
578 s.c.So(headers, ShouldResemble, exp.Headers)
579 s.c.So(string(body), ShouldEqual, exp.Body)
580
581 // Mocked reply.
582 if exp.Status != 0 {
583 for k, v := range exp.ResponseHeaders {
584 for _, s := range v {
585 w.Header().Add(k, s)
586 }
587 }
588 w.WriteHeader(exp.Status)
589 }
590 if exp.Reply != "" {
591 w.Write([]byte(exp.Reply))
592 }
593 s.index++
594 }
OLDNEW
« no previous file with comments | « go/src/infra/tools/cipd/client.go ('k') | go/src/infra/tools/cipd/clock.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698