Merge branch '11807-yaml-to-json'
[arvados.git] / services / keepstore / handler_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 // Tests for Keep HTTP handlers:
6 //
7 //     GetBlockHandler
8 //     PutBlockHandler
9 //     IndexHandler
10 //
11 // The HTTP handlers are responsible for enforcing permission policy,
12 // so these tests must exercise all possible permission permutations.
13
14 package main
15
16 import (
17         "bytes"
18         "context"
19         "encoding/json"
20         "fmt"
21         "net/http"
22         "net/http/httptest"
23         "os"
24         "regexp"
25         "strings"
26         "testing"
27         "time"
28
29         "git.curoverse.com/arvados.git/sdk/go/arvados"
30 )
31
32 // A RequestTester represents the parameters for an HTTP request to
33 // be issued on behalf of a unit test.
34 type RequestTester struct {
35         uri         string
36         apiToken    string
37         method      string
38         requestBody []byte
39 }
40
41 // Test GetBlockHandler on the following situations:
42 //   - permissions off, unauthenticated request, unsigned locator
43 //   - permissions on, authenticated request, signed locator
44 //   - permissions on, authenticated request, unsigned locator
45 //   - permissions on, unauthenticated request, signed locator
46 //   - permissions on, authenticated request, expired locator
47 //
48 func TestGetHandler(t *testing.T) {
49         defer teardown()
50
51         // Prepare two test Keep volumes. Our block is stored on the second volume.
52         KeepVM = MakeTestVolumeManager(2)
53         defer KeepVM.Close()
54
55         vols := KeepVM.AllWritable()
56         if err := vols[0].Put(context.Background(), TestHash, TestBlock); err != nil {
57                 t.Error(err)
58         }
59
60         // Create locators for testing.
61         // Turn on permission settings so we can generate signed locators.
62         theConfig.RequireSignatures = true
63         theConfig.blobSigningKey = []byte(knownKey)
64         theConfig.BlobSignatureTTL.Set("5m")
65
66         var (
67                 unsignedLocator  = "/" + TestHash
68                 validTimestamp   = time.Now().Add(theConfig.BlobSignatureTTL.Duration())
69                 expiredTimestamp = time.Now().Add(-time.Hour)
70                 signedLocator    = "/" + SignLocator(TestHash, knownToken, validTimestamp)
71                 expiredLocator   = "/" + SignLocator(TestHash, knownToken, expiredTimestamp)
72         )
73
74         // -----------------
75         // Test unauthenticated request with permissions off.
76         theConfig.RequireSignatures = false
77
78         // Unauthenticated request, unsigned locator
79         // => OK
80         response := IssueRequest(
81                 &RequestTester{
82                         method: "GET",
83                         uri:    unsignedLocator,
84                 })
85         ExpectStatusCode(t,
86                 "Unauthenticated request, unsigned locator", http.StatusOK, response)
87         ExpectBody(t,
88                 "Unauthenticated request, unsigned locator",
89                 string(TestBlock),
90                 response)
91
92         receivedLen := response.Header().Get("Content-Length")
93         expectedLen := fmt.Sprintf("%d", len(TestBlock))
94         if receivedLen != expectedLen {
95                 t.Errorf("expected Content-Length %s, got %s", expectedLen, receivedLen)
96         }
97
98         // ----------------
99         // Permissions: on.
100         theConfig.RequireSignatures = true
101
102         // Authenticated request, signed locator
103         // => OK
104         response = IssueRequest(&RequestTester{
105                 method:   "GET",
106                 uri:      signedLocator,
107                 apiToken: knownToken,
108         })
109         ExpectStatusCode(t,
110                 "Authenticated request, signed locator", http.StatusOK, response)
111         ExpectBody(t,
112                 "Authenticated request, signed locator", string(TestBlock), response)
113
114         receivedLen = response.Header().Get("Content-Length")
115         expectedLen = fmt.Sprintf("%d", len(TestBlock))
116         if receivedLen != expectedLen {
117                 t.Errorf("expected Content-Length %s, got %s", expectedLen, receivedLen)
118         }
119
120         // Authenticated request, unsigned locator
121         // => PermissionError
122         response = IssueRequest(&RequestTester{
123                 method:   "GET",
124                 uri:      unsignedLocator,
125                 apiToken: knownToken,
126         })
127         ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response)
128
129         // Unauthenticated request, signed locator
130         // => PermissionError
131         response = IssueRequest(&RequestTester{
132                 method: "GET",
133                 uri:    signedLocator,
134         })
135         ExpectStatusCode(t,
136                 "Unauthenticated request, signed locator",
137                 PermissionError.HTTPCode, response)
138
139         // Authenticated request, expired locator
140         // => ExpiredError
141         response = IssueRequest(&RequestTester{
142                 method:   "GET",
143                 uri:      expiredLocator,
144                 apiToken: knownToken,
145         })
146         ExpectStatusCode(t,
147                 "Authenticated request, expired locator",
148                 ExpiredError.HTTPCode, response)
149 }
150
151 // Test PutBlockHandler on the following situations:
152 //   - no server key
153 //   - with server key, authenticated request, unsigned locator
154 //   - with server key, unauthenticated request, unsigned locator
155 //
156 func TestPutHandler(t *testing.T) {
157         defer teardown()
158
159         // Prepare two test Keep volumes.
160         KeepVM = MakeTestVolumeManager(2)
161         defer KeepVM.Close()
162
163         // --------------
164         // No server key.
165
166         // Unauthenticated request, no server key
167         // => OK (unsigned response)
168         unsignedLocator := "/" + TestHash
169         response := IssueRequest(
170                 &RequestTester{
171                         method:      "PUT",
172                         uri:         unsignedLocator,
173                         requestBody: TestBlock,
174                 })
175
176         ExpectStatusCode(t,
177                 "Unauthenticated request, no server key", http.StatusOK, response)
178         ExpectBody(t,
179                 "Unauthenticated request, no server key",
180                 TestHashPutResp, response)
181
182         // ------------------
183         // With a server key.
184
185         theConfig.blobSigningKey = []byte(knownKey)
186         theConfig.BlobSignatureTTL.Set("5m")
187
188         // When a permission key is available, the locator returned
189         // from an authenticated PUT request will be signed.
190
191         // Authenticated PUT, signed locator
192         // => OK (signed response)
193         response = IssueRequest(
194                 &RequestTester{
195                         method:      "PUT",
196                         uri:         unsignedLocator,
197                         requestBody: TestBlock,
198                         apiToken:    knownToken,
199                 })
200
201         ExpectStatusCode(t,
202                 "Authenticated PUT, signed locator, with server key",
203                 http.StatusOK, response)
204         responseLocator := strings.TrimSpace(response.Body.String())
205         if VerifySignature(responseLocator, knownToken) != nil {
206                 t.Errorf("Authenticated PUT, signed locator, with server key:\n"+
207                         "response '%s' does not contain a valid signature",
208                         responseLocator)
209         }
210
211         // Unauthenticated PUT, unsigned locator
212         // => OK
213         response = IssueRequest(
214                 &RequestTester{
215                         method:      "PUT",
216                         uri:         unsignedLocator,
217                         requestBody: TestBlock,
218                 })
219
220         ExpectStatusCode(t,
221                 "Unauthenticated PUT, unsigned locator, with server key",
222                 http.StatusOK, response)
223         ExpectBody(t,
224                 "Unauthenticated PUT, unsigned locator, with server key",
225                 TestHashPutResp, response)
226 }
227
228 func TestPutAndDeleteSkipReadonlyVolumes(t *testing.T) {
229         defer teardown()
230         theConfig.systemAuthToken = "fake-data-manager-token"
231         vols := []*MockVolume{CreateMockVolume(), CreateMockVolume()}
232         vols[0].Readonly = true
233         KeepVM = MakeRRVolumeManager([]Volume{vols[0], vols[1]})
234         defer KeepVM.Close()
235         IssueRequest(
236                 &RequestTester{
237                         method:      "PUT",
238                         uri:         "/" + TestHash,
239                         requestBody: TestBlock,
240                 })
241         defer func(orig bool) {
242                 theConfig.EnableDelete = orig
243         }(theConfig.EnableDelete)
244         theConfig.EnableDelete = true
245         IssueRequest(
246                 &RequestTester{
247                         method:      "DELETE",
248                         uri:         "/" + TestHash,
249                         requestBody: TestBlock,
250                         apiToken:    theConfig.systemAuthToken,
251                 })
252         type expect struct {
253                 volnum    int
254                 method    string
255                 callcount int
256         }
257         for _, e := range []expect{
258                 {0, "Get", 0},
259                 {0, "Compare", 0},
260                 {0, "Touch", 0},
261                 {0, "Put", 0},
262                 {0, "Delete", 0},
263                 {1, "Get", 0},
264                 {1, "Compare", 1},
265                 {1, "Touch", 1},
266                 {1, "Put", 1},
267                 {1, "Delete", 1},
268         } {
269                 if calls := vols[e.volnum].CallCount(e.method); calls != e.callcount {
270                         t.Errorf("Got %d %s() on vol %d, expect %d", calls, e.method, e.volnum, e.callcount)
271                 }
272         }
273 }
274
275 // Test /index requests:
276 //   - unauthenticated /index request
277 //   - unauthenticated /index/prefix request
278 //   - authenticated   /index request        | non-superuser
279 //   - authenticated   /index/prefix request | non-superuser
280 //   - authenticated   /index request        | superuser
281 //   - authenticated   /index/prefix request | superuser
282 //
283 // The only /index requests that should succeed are those issued by the
284 // superuser. They should pass regardless of the value of RequireSignatures.
285 //
286 func TestIndexHandler(t *testing.T) {
287         defer teardown()
288
289         // Set up Keep volumes and populate them.
290         // Include multiple blocks on different volumes, and
291         // some metadata files (which should be omitted from index listings)
292         KeepVM = MakeTestVolumeManager(2)
293         defer KeepVM.Close()
294
295         vols := KeepVM.AllWritable()
296         vols[0].Put(context.Background(), TestHash, TestBlock)
297         vols[1].Put(context.Background(), TestHash2, TestBlock2)
298         vols[0].Put(context.Background(), TestHash+".meta", []byte("metadata"))
299         vols[1].Put(context.Background(), TestHash2+".meta", []byte("metadata"))
300
301         theConfig.systemAuthToken = "DATA MANAGER TOKEN"
302
303         unauthenticatedReq := &RequestTester{
304                 method: "GET",
305                 uri:    "/index",
306         }
307         authenticatedReq := &RequestTester{
308                 method:   "GET",
309                 uri:      "/index",
310                 apiToken: knownToken,
311         }
312         superuserReq := &RequestTester{
313                 method:   "GET",
314                 uri:      "/index",
315                 apiToken: theConfig.systemAuthToken,
316         }
317         unauthPrefixReq := &RequestTester{
318                 method: "GET",
319                 uri:    "/index/" + TestHash[0:3],
320         }
321         authPrefixReq := &RequestTester{
322                 method:   "GET",
323                 uri:      "/index/" + TestHash[0:3],
324                 apiToken: knownToken,
325         }
326         superuserPrefixReq := &RequestTester{
327                 method:   "GET",
328                 uri:      "/index/" + TestHash[0:3],
329                 apiToken: theConfig.systemAuthToken,
330         }
331         superuserNoSuchPrefixReq := &RequestTester{
332                 method:   "GET",
333                 uri:      "/index/abcd",
334                 apiToken: theConfig.systemAuthToken,
335         }
336         superuserInvalidPrefixReq := &RequestTester{
337                 method:   "GET",
338                 uri:      "/index/xyz",
339                 apiToken: theConfig.systemAuthToken,
340         }
341
342         // -------------------------------------------------------------
343         // Only the superuser should be allowed to issue /index requests.
344
345         // ---------------------------
346         // RequireSignatures enabled
347         // This setting should not affect tests passing.
348         theConfig.RequireSignatures = true
349
350         // unauthenticated /index request
351         // => UnauthorizedError
352         response := IssueRequest(unauthenticatedReq)
353         ExpectStatusCode(t,
354                 "RequireSignatures on, unauthenticated request",
355                 UnauthorizedError.HTTPCode,
356                 response)
357
358         // unauthenticated /index/prefix request
359         // => UnauthorizedError
360         response = IssueRequest(unauthPrefixReq)
361         ExpectStatusCode(t,
362                 "permissions on, unauthenticated /index/prefix request",
363                 UnauthorizedError.HTTPCode,
364                 response)
365
366         // authenticated /index request, non-superuser
367         // => UnauthorizedError
368         response = IssueRequest(authenticatedReq)
369         ExpectStatusCode(t,
370                 "permissions on, authenticated request, non-superuser",
371                 UnauthorizedError.HTTPCode,
372                 response)
373
374         // authenticated /index/prefix request, non-superuser
375         // => UnauthorizedError
376         response = IssueRequest(authPrefixReq)
377         ExpectStatusCode(t,
378                 "permissions on, authenticated /index/prefix request, non-superuser",
379                 UnauthorizedError.HTTPCode,
380                 response)
381
382         // superuser /index request
383         // => OK
384         response = IssueRequest(superuserReq)
385         ExpectStatusCode(t,
386                 "permissions on, superuser request",
387                 http.StatusOK,
388                 response)
389
390         // ----------------------------
391         // RequireSignatures disabled
392         // Valid Request should still pass.
393         theConfig.RequireSignatures = false
394
395         // superuser /index request
396         // => OK
397         response = IssueRequest(superuserReq)
398         ExpectStatusCode(t,
399                 "permissions on, superuser request",
400                 http.StatusOK,
401                 response)
402
403         expected := `^` + TestHash + `\+\d+ \d+\n` +
404                 TestHash2 + `\+\d+ \d+\n\n$`
405         match, _ := regexp.MatchString(expected, response.Body.String())
406         if !match {
407                 t.Errorf(
408                         "permissions on, superuser request: expected %s, got:\n%s",
409                         expected, response.Body.String())
410         }
411
412         // superuser /index/prefix request
413         // => OK
414         response = IssueRequest(superuserPrefixReq)
415         ExpectStatusCode(t,
416                 "permissions on, superuser request",
417                 http.StatusOK,
418                 response)
419
420         expected = `^` + TestHash + `\+\d+ \d+\n\n$`
421         match, _ = regexp.MatchString(expected, response.Body.String())
422         if !match {
423                 t.Errorf(
424                         "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
425                         expected, response.Body.String())
426         }
427
428         // superuser /index/{no-such-prefix} request
429         // => OK
430         response = IssueRequest(superuserNoSuchPrefixReq)
431         ExpectStatusCode(t,
432                 "permissions on, superuser request",
433                 http.StatusOK,
434                 response)
435
436         if "\n" != response.Body.String() {
437                 t.Errorf("Expected empty response for %s. Found %s", superuserNoSuchPrefixReq.uri, response.Body.String())
438         }
439
440         // superuser /index/{invalid-prefix} request
441         // => StatusBadRequest
442         response = IssueRequest(superuserInvalidPrefixReq)
443         ExpectStatusCode(t,
444                 "permissions on, superuser request",
445                 http.StatusBadRequest,
446                 response)
447 }
448
449 // TestDeleteHandler
450 //
451 // Cases tested:
452 //
453 //   With no token and with a non-data-manager token:
454 //   * Delete existing block
455 //     (test for 403 Forbidden, confirm block not deleted)
456 //
457 //   With data manager token:
458 //
459 //   * Delete existing block
460 //     (test for 200 OK, response counts, confirm block deleted)
461 //
462 //   * Delete nonexistent block
463 //     (test for 200 OK, response counts)
464 //
465 //   TODO(twp):
466 //
467 //   * Delete block on read-only and read-write volume
468 //     (test for 200 OK, response with copies_deleted=1,
469 //     copies_failed=1, confirm block deleted only on r/w volume)
470 //
471 //   * Delete block on read-only volume only
472 //     (test for 200 OK, response with copies_deleted=0, copies_failed=1,
473 //     confirm block not deleted)
474 //
475 func TestDeleteHandler(t *testing.T) {
476         defer teardown()
477
478         // Set up Keep volumes and populate them.
479         // Include multiple blocks on different volumes, and
480         // some metadata files (which should be omitted from index listings)
481         KeepVM = MakeTestVolumeManager(2)
482         defer KeepVM.Close()
483
484         vols := KeepVM.AllWritable()
485         vols[0].Put(context.Background(), TestHash, TestBlock)
486
487         // Explicitly set the BlobSignatureTTL to 0 for these
488         // tests, to ensure the MockVolume deletes the blocks
489         // even though they have just been created.
490         theConfig.BlobSignatureTTL = arvados.Duration(0)
491
492         var userToken = "NOT DATA MANAGER TOKEN"
493         theConfig.systemAuthToken = "DATA MANAGER TOKEN"
494
495         theConfig.EnableDelete = true
496
497         unauthReq := &RequestTester{
498                 method: "DELETE",
499                 uri:    "/" + TestHash,
500         }
501
502         userReq := &RequestTester{
503                 method:   "DELETE",
504                 uri:      "/" + TestHash,
505                 apiToken: userToken,
506         }
507
508         superuserExistingBlockReq := &RequestTester{
509                 method:   "DELETE",
510                 uri:      "/" + TestHash,
511                 apiToken: theConfig.systemAuthToken,
512         }
513
514         superuserNonexistentBlockReq := &RequestTester{
515                 method:   "DELETE",
516                 uri:      "/" + TestHash2,
517                 apiToken: theConfig.systemAuthToken,
518         }
519
520         // Unauthenticated request returns PermissionError.
521         var response *httptest.ResponseRecorder
522         response = IssueRequest(unauthReq)
523         ExpectStatusCode(t,
524                 "unauthenticated request",
525                 PermissionError.HTTPCode,
526                 response)
527
528         // Authenticated non-admin request returns PermissionError.
529         response = IssueRequest(userReq)
530         ExpectStatusCode(t,
531                 "authenticated non-admin request",
532                 PermissionError.HTTPCode,
533                 response)
534
535         // Authenticated admin request for nonexistent block.
536         type deletecounter struct {
537                 Deleted int `json:"copies_deleted"`
538                 Failed  int `json:"copies_failed"`
539         }
540         var responseDc, expectedDc deletecounter
541
542         response = IssueRequest(superuserNonexistentBlockReq)
543         ExpectStatusCode(t,
544                 "data manager request, nonexistent block",
545                 http.StatusNotFound,
546                 response)
547
548         // Authenticated admin request for existing block while EnableDelete is false.
549         theConfig.EnableDelete = false
550         response = IssueRequest(superuserExistingBlockReq)
551         ExpectStatusCode(t,
552                 "authenticated request, existing block, method disabled",
553                 MethodDisabledError.HTTPCode,
554                 response)
555         theConfig.EnableDelete = true
556
557         // Authenticated admin request for existing block.
558         response = IssueRequest(superuserExistingBlockReq)
559         ExpectStatusCode(t,
560                 "data manager request, existing block",
561                 http.StatusOK,
562                 response)
563         // Expect response {"copies_deleted":1,"copies_failed":0}
564         expectedDc = deletecounter{1, 0}
565         json.NewDecoder(response.Body).Decode(&responseDc)
566         if responseDc != expectedDc {
567                 t.Errorf("superuserExistingBlockReq\nexpected: %+v\nreceived: %+v",
568                         expectedDc, responseDc)
569         }
570         // Confirm the block has been deleted
571         buf := make([]byte, BlockSize)
572         _, err := vols[0].Get(context.Background(), TestHash, buf)
573         var blockDeleted = os.IsNotExist(err)
574         if !blockDeleted {
575                 t.Error("superuserExistingBlockReq: block not deleted")
576         }
577
578         // A DELETE request on a block newer than BlobSignatureTTL
579         // should return success but leave the block on the volume.
580         vols[0].Put(context.Background(), TestHash, TestBlock)
581         theConfig.BlobSignatureTTL = arvados.Duration(time.Hour)
582
583         response = IssueRequest(superuserExistingBlockReq)
584         ExpectStatusCode(t,
585                 "data manager request, existing block",
586                 http.StatusOK,
587                 response)
588         // Expect response {"copies_deleted":1,"copies_failed":0}
589         expectedDc = deletecounter{1, 0}
590         json.NewDecoder(response.Body).Decode(&responseDc)
591         if responseDc != expectedDc {
592                 t.Errorf("superuserExistingBlockReq\nexpected: %+v\nreceived: %+v",
593                         expectedDc, responseDc)
594         }
595         // Confirm the block has NOT been deleted.
596         _, err = vols[0].Get(context.Background(), TestHash, buf)
597         if err != nil {
598                 t.Errorf("testing delete on new block: %s\n", err)
599         }
600 }
601
602 // TestPullHandler
603 //
604 // Test handling of the PUT /pull statement.
605 //
606 // Cases tested: syntactically valid and invalid pull lists, from the
607 // data manager and from unprivileged users:
608 //
609 //   1. Valid pull list from an ordinary user
610 //      (expected result: 401 Unauthorized)
611 //
612 //   2. Invalid pull request from an ordinary user
613 //      (expected result: 401 Unauthorized)
614 //
615 //   3. Valid pull request from the data manager
616 //      (expected result: 200 OK with request body "Received 3 pull
617 //      requests"
618 //
619 //   4. Invalid pull request from the data manager
620 //      (expected result: 400 Bad Request)
621 //
622 // Test that in the end, the pull manager received a good pull list with
623 // the expected number of requests.
624 //
625 // TODO(twp): test concurrency: launch 100 goroutines to update the
626 // pull list simultaneously.  Make sure that none of them return 400
627 // Bad Request and that pullq.GetList() returns a valid list.
628 //
629 func TestPullHandler(t *testing.T) {
630         defer teardown()
631
632         var userToken = "USER TOKEN"
633         theConfig.systemAuthToken = "DATA MANAGER TOKEN"
634
635         pullq = NewWorkQueue()
636
637         goodJSON := []byte(`[
638                 {
639                         "locator":"locator_with_two_servers",
640                         "servers":[
641                                 "server1",
642                                 "server2"
643                         ]
644                 },
645                 {
646                         "locator":"locator_with_no_servers",
647                         "servers":[]
648                 },
649                 {
650                         "locator":"",
651                         "servers":["empty_locator"]
652                 }
653         ]`)
654
655         badJSON := []byte(`{ "key":"I'm a little teapot" }`)
656
657         type pullTest struct {
658                 name         string
659                 req          RequestTester
660                 responseCode int
661                 responseBody string
662         }
663         var testcases = []pullTest{
664                 {
665                         "Valid pull list from an ordinary user",
666                         RequestTester{"/pull", userToken, "PUT", goodJSON},
667                         http.StatusUnauthorized,
668                         "Unauthorized\n",
669                 },
670                 {
671                         "Invalid pull request from an ordinary user",
672                         RequestTester{"/pull", userToken, "PUT", badJSON},
673                         http.StatusUnauthorized,
674                         "Unauthorized\n",
675                 },
676                 {
677                         "Valid pull request from the data manager",
678                         RequestTester{"/pull", theConfig.systemAuthToken, "PUT", goodJSON},
679                         http.StatusOK,
680                         "Received 3 pull requests\n",
681                 },
682                 {
683                         "Invalid pull request from the data manager",
684                         RequestTester{"/pull", theConfig.systemAuthToken, "PUT", badJSON},
685                         http.StatusBadRequest,
686                         "",
687                 },
688         }
689
690         for _, tst := range testcases {
691                 response := IssueRequest(&tst.req)
692                 ExpectStatusCode(t, tst.name, tst.responseCode, response)
693                 ExpectBody(t, tst.name, tst.responseBody, response)
694         }
695
696         // The Keep pull manager should have received one good list with 3
697         // requests on it.
698         for i := 0; i < 3; i++ {
699                 item := <-pullq.NextItem
700                 if _, ok := item.(PullRequest); !ok {
701                         t.Errorf("item %v could not be parsed as a PullRequest", item)
702                 }
703         }
704
705         expectChannelEmpty(t, pullq.NextItem)
706 }
707
708 // TestTrashHandler
709 //
710 // Test cases:
711 //
712 // Cases tested: syntactically valid and invalid trash lists, from the
713 // data manager and from unprivileged users:
714 //
715 //   1. Valid trash list from an ordinary user
716 //      (expected result: 401 Unauthorized)
717 //
718 //   2. Invalid trash list from an ordinary user
719 //      (expected result: 401 Unauthorized)
720 //
721 //   3. Valid trash list from the data manager
722 //      (expected result: 200 OK with request body "Received 3 trash
723 //      requests"
724 //
725 //   4. Invalid trash list from the data manager
726 //      (expected result: 400 Bad Request)
727 //
728 // Test that in the end, the trash collector received a good list
729 // trash list with the expected number of requests.
730 //
731 // TODO(twp): test concurrency: launch 100 goroutines to update the
732 // pull list simultaneously.  Make sure that none of them return 400
733 // Bad Request and that replica.Dump() returns a valid list.
734 //
735 func TestTrashHandler(t *testing.T) {
736         defer teardown()
737
738         var userToken = "USER TOKEN"
739         theConfig.systemAuthToken = "DATA MANAGER TOKEN"
740
741         trashq = NewWorkQueue()
742
743         goodJSON := []byte(`[
744                 {
745                         "locator":"block1",
746                         "block_mtime":1409082153
747                 },
748                 {
749                         "locator":"block2",
750                         "block_mtime":1409082153
751                 },
752                 {
753                         "locator":"block3",
754                         "block_mtime":1409082153
755                 }
756         ]`)
757
758         badJSON := []byte(`I am not a valid JSON string`)
759
760         type trashTest struct {
761                 name         string
762                 req          RequestTester
763                 responseCode int
764                 responseBody string
765         }
766
767         var testcases = []trashTest{
768                 {
769                         "Valid trash list from an ordinary user",
770                         RequestTester{"/trash", userToken, "PUT", goodJSON},
771                         http.StatusUnauthorized,
772                         "Unauthorized\n",
773                 },
774                 {
775                         "Invalid trash list from an ordinary user",
776                         RequestTester{"/trash", userToken, "PUT", badJSON},
777                         http.StatusUnauthorized,
778                         "Unauthorized\n",
779                 },
780                 {
781                         "Valid trash list from the data manager",
782                         RequestTester{"/trash", theConfig.systemAuthToken, "PUT", goodJSON},
783                         http.StatusOK,
784                         "Received 3 trash requests\n",
785                 },
786                 {
787                         "Invalid trash list from the data manager",
788                         RequestTester{"/trash", theConfig.systemAuthToken, "PUT", badJSON},
789                         http.StatusBadRequest,
790                         "",
791                 },
792         }
793
794         for _, tst := range testcases {
795                 response := IssueRequest(&tst.req)
796                 ExpectStatusCode(t, tst.name, tst.responseCode, response)
797                 ExpectBody(t, tst.name, tst.responseBody, response)
798         }
799
800         // The trash collector should have received one good list with 3
801         // requests on it.
802         for i := 0; i < 3; i++ {
803                 item := <-trashq.NextItem
804                 if _, ok := item.(TrashRequest); !ok {
805                         t.Errorf("item %v could not be parsed as a TrashRequest", item)
806                 }
807         }
808
809         expectChannelEmpty(t, trashq.NextItem)
810 }
811
812 // ====================
813 // Helper functions
814 // ====================
815
816 // IssueTestRequest executes an HTTP request described by rt, to a
817 // REST router.  It returns the HTTP response to the request.
818 func IssueRequest(rt *RequestTester) *httptest.ResponseRecorder {
819         response := httptest.NewRecorder()
820         body := bytes.NewReader(rt.requestBody)
821         req, _ := http.NewRequest(rt.method, rt.uri, body)
822         if rt.apiToken != "" {
823                 req.Header.Set("Authorization", "OAuth2 "+rt.apiToken)
824         }
825         loggingRouter := MakeRESTRouter()
826         loggingRouter.ServeHTTP(response, req)
827         return response
828 }
829
830 // ExpectStatusCode checks whether a response has the specified status code,
831 // and reports a test failure if not.
832 func ExpectStatusCode(
833         t *testing.T,
834         testname string,
835         expectedStatus int,
836         response *httptest.ResponseRecorder) {
837         if response.Code != expectedStatus {
838                 t.Errorf("%s: expected status %d, got %+v",
839                         testname, expectedStatus, response)
840         }
841 }
842
843 func ExpectBody(
844         t *testing.T,
845         testname string,
846         expectedBody string,
847         response *httptest.ResponseRecorder) {
848         if expectedBody != "" && response.Body.String() != expectedBody {
849                 t.Errorf("%s: expected response body '%s', got %+v",
850                         testname, expectedBody, response)
851         }
852 }
853
854 // See #7121
855 func TestPutNeedsOnlyOneBuffer(t *testing.T) {
856         defer teardown()
857         KeepVM = MakeTestVolumeManager(1)
858         defer KeepVM.Close()
859
860         defer func(orig *bufferPool) {
861                 bufs = orig
862         }(bufs)
863         bufs = newBufferPool(1, BlockSize)
864
865         ok := make(chan struct{})
866         go func() {
867                 for i := 0; i < 2; i++ {
868                         response := IssueRequest(
869                                 &RequestTester{
870                                         method:      "PUT",
871                                         uri:         "/" + TestHash,
872                                         requestBody: TestBlock,
873                                 })
874                         ExpectStatusCode(t,
875                                 "TestPutNeedsOnlyOneBuffer", http.StatusOK, response)
876                 }
877                 ok <- struct{}{}
878         }()
879
880         select {
881         case <-ok:
882         case <-time.After(time.Second):
883                 t.Fatal("PUT deadlocks with MaxBuffers==1")
884         }
885 }
886
887 // Invoke the PutBlockHandler a bunch of times to test for bufferpool resource
888 // leak.
889 func TestPutHandlerNoBufferleak(t *testing.T) {
890         defer teardown()
891
892         // Prepare two test Keep volumes.
893         KeepVM = MakeTestVolumeManager(2)
894         defer KeepVM.Close()
895
896         ok := make(chan bool)
897         go func() {
898                 for i := 0; i < theConfig.MaxBuffers+1; i++ {
899                         // Unauthenticated request, no server key
900                         // => OK (unsigned response)
901                         unsignedLocator := "/" + TestHash
902                         response := IssueRequest(
903                                 &RequestTester{
904                                         method:      "PUT",
905                                         uri:         unsignedLocator,
906                                         requestBody: TestBlock,
907                                 })
908                         ExpectStatusCode(t,
909                                 "TestPutHandlerBufferleak", http.StatusOK, response)
910                         ExpectBody(t,
911                                 "TestPutHandlerBufferleak",
912                                 TestHashPutResp, response)
913                 }
914                 ok <- true
915         }()
916         select {
917         case <-time.After(20 * time.Second):
918                 // If the buffer pool leaks, the test goroutine hangs.
919                 t.Fatal("test did not finish, assuming pool leaked")
920         case <-ok:
921         }
922 }
923
924 type notifyingResponseRecorder struct {
925         *httptest.ResponseRecorder
926         closer chan bool
927 }
928
929 func (r *notifyingResponseRecorder) CloseNotify() <-chan bool {
930         return r.closer
931 }
932
933 func TestGetHandlerClientDisconnect(t *testing.T) {
934         defer func(was bool) {
935                 theConfig.RequireSignatures = was
936         }(theConfig.RequireSignatures)
937         theConfig.RequireSignatures = false
938
939         defer func(orig *bufferPool) {
940                 bufs = orig
941         }(bufs)
942         bufs = newBufferPool(1, BlockSize)
943         defer bufs.Put(bufs.Get(BlockSize))
944
945         KeepVM = MakeTestVolumeManager(2)
946         defer KeepVM.Close()
947
948         if err := KeepVM.AllWritable()[0].Put(context.Background(), TestHash, TestBlock); err != nil {
949                 t.Error(err)
950         }
951
952         resp := &notifyingResponseRecorder{
953                 ResponseRecorder: httptest.NewRecorder(),
954                 closer:           make(chan bool, 1),
955         }
956         if _, ok := http.ResponseWriter(resp).(http.CloseNotifier); !ok {
957                 t.Fatal("notifyingResponseRecorder is broken")
958         }
959         // If anyone asks, the client has disconnected.
960         resp.closer <- true
961
962         ok := make(chan struct{})
963         go func() {
964                 req, _ := http.NewRequest("GET", fmt.Sprintf("/%s+%d", TestHash, len(TestBlock)), nil)
965                 (&LoggingRESTRouter{router: MakeRESTRouter()}).ServeHTTP(resp, req)
966                 ok <- struct{}{}
967         }()
968
969         select {
970         case <-time.After(20 * time.Second):
971                 t.Fatal("request took >20s, close notifier must be broken")
972         case <-ok:
973         }
974
975         ExpectStatusCode(t, "client disconnect", http.StatusServiceUnavailable, resp.ResponseRecorder)
976         for i, v := range KeepVM.AllWritable() {
977                 if calls := v.(*MockVolume).called["GET"]; calls != 0 {
978                         t.Errorf("volume %d got %d calls, expected 0", i, calls)
979                 }
980         }
981 }
982
983 // Invoke the GetBlockHandler a bunch of times to test for bufferpool resource
984 // leak.
985 func TestGetHandlerNoBufferLeak(t *testing.T) {
986         defer teardown()
987
988         // Prepare two test Keep volumes. Our block is stored on the second volume.
989         KeepVM = MakeTestVolumeManager(2)
990         defer KeepVM.Close()
991
992         vols := KeepVM.AllWritable()
993         if err := vols[0].Put(context.Background(), TestHash, TestBlock); err != nil {
994                 t.Error(err)
995         }
996
997         ok := make(chan bool)
998         go func() {
999                 for i := 0; i < theConfig.MaxBuffers+1; i++ {
1000                         // Unauthenticated request, unsigned locator
1001                         // => OK
1002                         unsignedLocator := "/" + TestHash
1003                         response := IssueRequest(
1004                                 &RequestTester{
1005                                         method: "GET",
1006                                         uri:    unsignedLocator,
1007                                 })
1008                         ExpectStatusCode(t,
1009                                 "Unauthenticated request, unsigned locator", http.StatusOK, response)
1010                         ExpectBody(t,
1011                                 "Unauthenticated request, unsigned locator",
1012                                 string(TestBlock),
1013                                 response)
1014                 }
1015                 ok <- true
1016         }()
1017         select {
1018         case <-time.After(20 * time.Second):
1019                 // If the buffer pool leaks, the test goroutine hangs.
1020                 t.Fatal("test did not finish, assuming pool leaked")
1021         case <-ok:
1022         }
1023 }
1024
1025 func TestPutReplicationHeader(t *testing.T) {
1026         defer teardown()
1027
1028         KeepVM = MakeTestVolumeManager(2)
1029         defer KeepVM.Close()
1030
1031         resp := IssueRequest(&RequestTester{
1032                 method:      "PUT",
1033                 uri:         "/" + TestHash,
1034                 requestBody: TestBlock,
1035         })
1036         if r := resp.Header().Get("X-Keep-Replicas-Stored"); r != "1" {
1037                 t.Errorf("Got X-Keep-Replicas-Stored: %q, expected %q", r, "1")
1038         }
1039 }
1040
1041 func TestUntrashHandler(t *testing.T) {
1042         defer teardown()
1043
1044         // Set up Keep volumes
1045         KeepVM = MakeTestVolumeManager(2)
1046         defer KeepVM.Close()
1047         vols := KeepVM.AllWritable()
1048         vols[0].Put(context.Background(), TestHash, TestBlock)
1049
1050         theConfig.systemAuthToken = "DATA MANAGER TOKEN"
1051
1052         // unauthenticatedReq => UnauthorizedError
1053         unauthenticatedReq := &RequestTester{
1054                 method: "PUT",
1055                 uri:    "/untrash/" + TestHash,
1056         }
1057         response := IssueRequest(unauthenticatedReq)
1058         ExpectStatusCode(t,
1059                 "Unauthenticated request",
1060                 UnauthorizedError.HTTPCode,
1061                 response)
1062
1063         // notDataManagerReq => UnauthorizedError
1064         notDataManagerReq := &RequestTester{
1065                 method:   "PUT",
1066                 uri:      "/untrash/" + TestHash,
1067                 apiToken: knownToken,
1068         }
1069
1070         response = IssueRequest(notDataManagerReq)
1071         ExpectStatusCode(t,
1072                 "Non-datamanager token",
1073                 UnauthorizedError.HTTPCode,
1074                 response)
1075
1076         // datamanagerWithBadHashReq => StatusBadRequest
1077         datamanagerWithBadHashReq := &RequestTester{
1078                 method:   "PUT",
1079                 uri:      "/untrash/thisisnotalocator",
1080                 apiToken: theConfig.systemAuthToken,
1081         }
1082         response = IssueRequest(datamanagerWithBadHashReq)
1083         ExpectStatusCode(t,
1084                 "Bad locator in untrash request",
1085                 http.StatusBadRequest,
1086                 response)
1087
1088         // datamanagerWrongMethodReq => StatusBadRequest
1089         datamanagerWrongMethodReq := &RequestTester{
1090                 method:   "GET",
1091                 uri:      "/untrash/" + TestHash,
1092                 apiToken: theConfig.systemAuthToken,
1093         }
1094         response = IssueRequest(datamanagerWrongMethodReq)
1095         ExpectStatusCode(t,
1096                 "Only PUT method is supported for untrash",
1097                 http.StatusBadRequest,
1098                 response)
1099
1100         // datamanagerReq => StatusOK
1101         datamanagerReq := &RequestTester{
1102                 method:   "PUT",
1103                 uri:      "/untrash/" + TestHash,
1104                 apiToken: theConfig.systemAuthToken,
1105         }
1106         response = IssueRequest(datamanagerReq)
1107         ExpectStatusCode(t,
1108                 "",
1109                 http.StatusOK,
1110                 response)
1111         expected := "Successfully untrashed on: [MockVolume],[MockVolume]"
1112         if response.Body.String() != expected {
1113                 t.Errorf(
1114                         "Untrash response mismatched: expected %s, got:\n%s",
1115                         expected, response.Body.String())
1116         }
1117 }
1118
1119 func TestUntrashHandlerWithNoWritableVolumes(t *testing.T) {
1120         defer teardown()
1121
1122         // Set up readonly Keep volumes
1123         vols := []*MockVolume{CreateMockVolume(), CreateMockVolume()}
1124         vols[0].Readonly = true
1125         vols[1].Readonly = true
1126         KeepVM = MakeRRVolumeManager([]Volume{vols[0], vols[1]})
1127         defer KeepVM.Close()
1128
1129         theConfig.systemAuthToken = "DATA MANAGER TOKEN"
1130
1131         // datamanagerReq => StatusOK
1132         datamanagerReq := &RequestTester{
1133                 method:   "PUT",
1134                 uri:      "/untrash/" + TestHash,
1135                 apiToken: theConfig.systemAuthToken,
1136         }
1137         response := IssueRequest(datamanagerReq)
1138         ExpectStatusCode(t,
1139                 "No writable volumes",
1140                 http.StatusNotFound,
1141                 response)
1142 }