1 // Tests for Keep HTTP handlers:
7 // The HTTP handlers are responsible for enforcing permission policy,
8 // so these tests must exercise all possible permission permutations.
16 "github.com/gorilla/mux"
26 // A RequestTester represents the parameters for an HTTP request to
27 // be issued on behalf of a unit test.
28 type RequestTester struct {
35 // Test GetBlockHandler on the following situations:
36 // - permissions off, unauthenticated request, unsigned locator
37 // - permissions on, authenticated request, signed locator
38 // - permissions on, authenticated request, unsigned locator
39 // - permissions on, unauthenticated request, signed locator
40 // - permissions on, authenticated request, expired locator
42 func TestGetHandler(t *testing.T) {
45 // Prepare two test Keep volumes. Our block is stored on the second volume.
46 KeepVM = MakeTestVolumeManager(2)
49 vols := KeepVM.Volumes()
50 if err := vols[0].Put(TEST_HASH, TEST_BLOCK); err != nil {
54 // Set up a REST router for testing the handlers.
55 rest := MakeRESTRouter()
57 // Create locators for testing.
58 // Turn on permission settings so we can generate signed locators.
59 enforce_permissions = true
60 PermissionSecret = []byte(known_key)
61 permission_ttl = time.Duration(300) * time.Second
64 unsigned_locator = "/" + TEST_HASH
65 valid_timestamp = time.Now().Add(permission_ttl)
66 expired_timestamp = time.Now().Add(-time.Hour)
67 signed_locator = "/" + SignLocator(TEST_HASH, known_token, valid_timestamp)
68 expired_locator = "/" + SignLocator(TEST_HASH, known_token, expired_timestamp)
72 // Test unauthenticated request with permissions off.
73 enforce_permissions = false
75 // Unauthenticated request, unsigned locator
77 response := IssueRequest(rest,
80 uri: unsigned_locator,
83 "Unauthenticated request, unsigned locator", http.StatusOK, response)
85 "Unauthenticated request, unsigned locator",
88 received_xbs := response.Header().Get("X-Block-Size")
89 expected_xbs := fmt.Sprintf("%d", len(TEST_BLOCK))
90 if received_xbs != expected_xbs {
91 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
96 enforce_permissions = true
98 // Authenticated request, signed locator
100 response = IssueRequest(rest, &RequestTester{
103 api_token: known_token,
106 "Authenticated request, signed locator", http.StatusOK, response)
108 "Authenticated request, signed locator", string(TEST_BLOCK), response)
109 received_xbs = response.Header().Get("X-Block-Size")
110 expected_xbs = fmt.Sprintf("%d", len(TEST_BLOCK))
111 if received_xbs != expected_xbs {
112 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
115 // Authenticated request, unsigned locator
116 // => PermissionError
117 response = IssueRequest(rest, &RequestTester{
119 uri: unsigned_locator,
120 api_token: known_token,
122 ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response)
124 // Unauthenticated request, signed locator
125 // => PermissionError
126 response = IssueRequest(rest, &RequestTester{
131 "Unauthenticated request, signed locator",
132 PermissionError.HTTPCode, response)
134 // Authenticated request, expired locator
136 response = IssueRequest(rest, &RequestTester{
138 uri: expired_locator,
139 api_token: known_token,
142 "Authenticated request, expired locator",
143 ExpiredError.HTTPCode, response)
146 // Test PutBlockHandler on the following situations:
148 // - with server key, authenticated request, unsigned locator
149 // - with server key, unauthenticated request, unsigned locator
151 func TestPutHandler(t *testing.T) {
154 // Prepare two test Keep volumes.
155 KeepVM = MakeTestVolumeManager(2)
158 // Set up a REST router for testing the handlers.
159 rest := MakeRESTRouter()
164 // Unauthenticated request, no server key
165 // => OK (unsigned response)
166 unsigned_locator := "/" + TEST_HASH
167 response := IssueRequest(rest,
170 uri: unsigned_locator,
171 request_body: TEST_BLOCK,
175 "Unauthenticated request, no server key", http.StatusOK, response)
177 "Unauthenticated request, no server key",
178 TEST_HASH_PUT_RESPONSE, response)
180 // ------------------
181 // With a server key.
183 PermissionSecret = []byte(known_key)
184 permission_ttl = time.Duration(300) * time.Second
186 // When a permission key is available, the locator returned
187 // from an authenticated PUT request will be signed.
189 // Authenticated PUT, signed locator
190 // => OK (signed response)
191 response = IssueRequest(rest,
194 uri: unsigned_locator,
195 request_body: TEST_BLOCK,
196 api_token: known_token,
200 "Authenticated PUT, signed locator, with server key",
201 http.StatusOK, response)
202 response_locator := strings.TrimSpace(response.Body.String())
203 if !VerifySignature(response_locator, known_token) {
204 t.Errorf("Authenticated PUT, signed locator, with server key:\n"+
205 "response '%s' does not contain a valid signature",
209 // Unauthenticated PUT, unsigned locator
211 response = IssueRequest(rest,
214 uri: unsigned_locator,
215 request_body: TEST_BLOCK,
219 "Unauthenticated PUT, unsigned locator, with server key",
220 http.StatusOK, response)
222 "Unauthenticated PUT, unsigned locator, with server key",
223 TEST_HASH_PUT_RESPONSE, response)
226 // Test /index requests:
227 // - unauthenticated /index request
228 // - unauthenticated /index/prefix request
229 // - authenticated /index request | non-superuser
230 // - authenticated /index/prefix request | non-superuser
231 // - authenticated /index request | superuser
232 // - authenticated /index/prefix request | superuser
234 // The only /index requests that should succeed are those issued by the
235 // superuser. They should pass regardless of the value of enforce_permissions.
237 func TestIndexHandler(t *testing.T) {
240 // Set up Keep volumes and populate them.
241 // Include multiple blocks on different volumes, and
242 // some metadata files (which should be omitted from index listings)
243 KeepVM = MakeTestVolumeManager(2)
246 vols := KeepVM.Volumes()
247 vols[0].Put(TEST_HASH, TEST_BLOCK)
248 vols[1].Put(TEST_HASH_2, TEST_BLOCK_2)
249 vols[0].Put(TEST_HASH+".meta", []byte("metadata"))
250 vols[1].Put(TEST_HASH_2+".meta", []byte("metadata"))
252 // Set up a REST router for testing the handlers.
253 rest := MakeRESTRouter()
255 data_manager_token = "DATA MANAGER TOKEN"
257 unauthenticated_req := &RequestTester{
261 authenticated_req := &RequestTester{
264 api_token: known_token,
266 superuser_req := &RequestTester{
269 api_token: data_manager_token,
271 unauth_prefix_req := &RequestTester{
273 uri: "/index/" + TEST_HASH[0:3],
275 auth_prefix_req := &RequestTester{
277 uri: "/index/" + TEST_HASH[0:3],
278 api_token: known_token,
280 superuser_prefix_req := &RequestTester{
282 uri: "/index/" + TEST_HASH[0:3],
283 api_token: data_manager_token,
286 // -------------------------------------------------------------
287 // Only the superuser should be allowed to issue /index requests.
289 // ---------------------------
290 // enforce_permissions enabled
291 // This setting should not affect tests passing.
292 enforce_permissions = true
294 // unauthenticated /index request
295 // => UnauthorizedError
296 response := IssueRequest(rest, unauthenticated_req)
298 "enforce_permissions on, unauthenticated request",
299 UnauthorizedError.HTTPCode,
302 // unauthenticated /index/prefix request
303 // => UnauthorizedError
304 response = IssueRequest(rest, unauth_prefix_req)
306 "permissions on, unauthenticated /index/prefix request",
307 UnauthorizedError.HTTPCode,
310 // authenticated /index request, non-superuser
311 // => UnauthorizedError
312 response = IssueRequest(rest, authenticated_req)
314 "permissions on, authenticated request, non-superuser",
315 UnauthorizedError.HTTPCode,
318 // authenticated /index/prefix request, non-superuser
319 // => UnauthorizedError
320 response = IssueRequest(rest, auth_prefix_req)
322 "permissions on, authenticated /index/prefix request, non-superuser",
323 UnauthorizedError.HTTPCode,
326 // superuser /index request
328 response = IssueRequest(rest, superuser_req)
330 "permissions on, superuser request",
334 // ----------------------------
335 // enforce_permissions disabled
336 // Valid Request should still pass.
337 enforce_permissions = false
339 // superuser /index request
341 response = IssueRequest(rest, superuser_req)
343 "permissions on, superuser request",
349 expected := `^` + TEST_HASH + `\+\d+ \d+\n` +
350 TEST_HASH_2 + `\+\d+ \d+\n$`
351 match, _ := regexp.MatchString(expected, response.Body.String())
354 "permissions on, superuser request: expected %s, got:\n%s",
355 expected, response.Body.String())
358 // superuser /index/prefix request
360 response = IssueRequest(rest, superuser_prefix_req)
362 "permissions on, superuser request",
366 expected = `^` + TEST_HASH + `\+\d+ \d+\n$`
367 match, _ = regexp.MatchString(expected, response.Body.String())
370 "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
371 expected, response.Body.String())
379 // With no token and with a non-data-manager token:
380 // * Delete existing block
381 // (test for 403 Forbidden, confirm block not deleted)
383 // With data manager token:
385 // * Delete existing block
386 // (test for 200 OK, response counts, confirm block deleted)
388 // * Delete nonexistent block
389 // (test for 200 OK, response counts)
393 // * Delete block on read-only and read-write volume
394 // (test for 200 OK, response with copies_deleted=1,
395 // copies_failed=1, confirm block deleted only on r/w volume)
397 // * Delete block on read-only volume only
398 // (test for 200 OK, response with copies_deleted=0, copies_failed=1,
399 // confirm block not deleted)
401 func TestDeleteHandler(t *testing.T) {
404 // Set up Keep volumes and populate them.
405 // Include multiple blocks on different volumes, and
406 // some metadata files (which should be omitted from index listings)
407 KeepVM = MakeTestVolumeManager(2)
410 vols := KeepVM.Volumes()
411 vols[0].Put(TEST_HASH, TEST_BLOCK)
413 // Explicitly set the permission_ttl to 0 for these
414 // tests, to ensure the MockVolume deletes the blocks
415 // even though they have just been created.
416 permission_ttl = time.Duration(0)
418 // Set up a REST router for testing the handlers.
419 rest := MakeRESTRouter()
421 var user_token = "NOT DATA MANAGER TOKEN"
422 data_manager_token = "DATA MANAGER TOKEN"
424 unauth_req := &RequestTester{
426 uri: "/" + TEST_HASH,
429 user_req := &RequestTester{
431 uri: "/" + TEST_HASH,
432 api_token: user_token,
435 superuser_existing_block_req := &RequestTester{
437 uri: "/" + TEST_HASH,
438 api_token: data_manager_token,
441 superuser_nonexistent_block_req := &RequestTester{
443 uri: "/" + TEST_HASH_2,
444 api_token: data_manager_token,
447 // Unauthenticated request returns PermissionError.
448 var response *httptest.ResponseRecorder
449 response = IssueRequest(rest, unauth_req)
451 "unauthenticated request",
452 PermissionError.HTTPCode,
455 // Authenticated non-admin request returns PermissionError.
456 response = IssueRequest(rest, user_req)
458 "authenticated non-admin request",
459 PermissionError.HTTPCode,
462 // Authenticated admin request for nonexistent block.
463 type deletecounter struct {
464 Deleted int `json:"copies_deleted"`
465 Failed int `json:"copies_failed"`
467 var response_dc, expected_dc deletecounter
469 response = IssueRequest(rest, superuser_nonexistent_block_req)
471 "data manager request, nonexistent block",
475 // Authenticated admin request for existing block while never_delete is set.
477 response = IssueRequest(rest, superuser_existing_block_req)
479 "authenticated request, existing block, method disabled",
480 MethodDisabledError.HTTPCode,
484 // Authenticated admin request for existing block.
485 response = IssueRequest(rest, superuser_existing_block_req)
487 "data manager request, existing block",
490 // Expect response {"copies_deleted":1,"copies_failed":0}
491 expected_dc = deletecounter{1, 0}
492 json.NewDecoder(response.Body).Decode(&response_dc)
493 if response_dc != expected_dc {
494 t.Errorf("superuser_existing_block_req\nexpected: %+v\nreceived: %+v",
495 expected_dc, response_dc)
497 // Confirm the block has been deleted
498 _, err := vols[0].Get(TEST_HASH)
499 var block_deleted = os.IsNotExist(err)
501 t.Error("superuser_existing_block_req: block not deleted")
504 // A DELETE request on a block newer than permission_ttl should return
505 // success but leave the block on the volume.
506 vols[0].Put(TEST_HASH, TEST_BLOCK)
507 permission_ttl = time.Duration(1) * time.Hour
509 response = IssueRequest(rest, superuser_existing_block_req)
511 "data manager request, existing block",
514 // Expect response {"copies_deleted":1,"copies_failed":0}
515 expected_dc = deletecounter{1, 0}
516 json.NewDecoder(response.Body).Decode(&response_dc)
517 if response_dc != expected_dc {
518 t.Errorf("superuser_existing_block_req\nexpected: %+v\nreceived: %+v",
519 expected_dc, response_dc)
521 // Confirm the block has NOT been deleted.
522 _, err = vols[0].Get(TEST_HASH)
524 t.Errorf("testing delete on new block: %s\n", err)
530 // Test handling of the PUT /pull statement.
532 // Cases tested: syntactically valid and invalid pull lists, from the
533 // data manager and from unprivileged users:
535 // 1. Valid pull list from an ordinary user
536 // (expected result: 401 Unauthorized)
538 // 2. Invalid pull request from an ordinary user
539 // (expected result: 401 Unauthorized)
541 // 3. Valid pull request from the data manager
542 // (expected result: 200 OK with request body "Received 3 pull
545 // 4. Invalid pull request from the data manager
546 // (expected result: 400 Bad Request)
548 // Test that in the end, the pull manager received a good pull list with
549 // the expected number of requests.
551 // TODO(twp): test concurrency: launch 100 goroutines to update the
552 // pull list simultaneously. Make sure that none of them return 400
553 // Bad Request and that pullq.GetList() returns a valid list.
555 func TestPullHandler(t *testing.T) {
558 // Set up a REST router for testing the handlers.
559 rest := MakeRESTRouter()
561 var user_token = "USER TOKEN"
562 data_manager_token = "DATA MANAGER TOKEN"
564 good_json := []byte(`[
566 "locator":"locator_with_two_servers",
573 "locator":"locator_with_no_servers",
578 "servers":["empty_locator"]
582 bad_json := []byte(`{ "key":"I'm a little teapot" }`)
584 type pullTest struct {
590 var testcases = []pullTest{
592 "Valid pull list from an ordinary user",
593 RequestTester{"/pull", user_token, "PUT", good_json},
594 http.StatusUnauthorized,
598 "Invalid pull request from an ordinary user",
599 RequestTester{"/pull", user_token, "PUT", bad_json},
600 http.StatusUnauthorized,
604 "Valid pull request from the data manager",
605 RequestTester{"/pull", data_manager_token, "PUT", good_json},
607 "Received 3 pull requests\n",
610 "Invalid pull request from the data manager",
611 RequestTester{"/pull", data_manager_token, "PUT", bad_json},
612 http.StatusBadRequest,
617 for _, tst := range testcases {
618 response := IssueRequest(rest, &tst.req)
619 ExpectStatusCode(t, tst.name, tst.response_code, response)
620 ExpectBody(t, tst.name, tst.response_body, response)
623 // The Keep pull manager should have received one good list with 3
625 for i := 0; i < 3; i++ {
626 item := <-pullq.NextItem
627 if _, ok := item.(PullRequest); !ok {
628 t.Errorf("item %v could not be parsed as a PullRequest", item)
632 expectChannelEmpty(t, pullq.NextItem)
639 // Cases tested: syntactically valid and invalid trash lists, from the
640 // data manager and from unprivileged users:
642 // 1. Valid trash list from an ordinary user
643 // (expected result: 401 Unauthorized)
645 // 2. Invalid trash list from an ordinary user
646 // (expected result: 401 Unauthorized)
648 // 3. Valid trash list from the data manager
649 // (expected result: 200 OK with request body "Received 3 trash
652 // 4. Invalid trash list from the data manager
653 // (expected result: 400 Bad Request)
655 // Test that in the end, the trash collector received a good list
656 // trash list with the expected number of requests.
658 // TODO(twp): test concurrency: launch 100 goroutines to update the
659 // pull list simultaneously. Make sure that none of them return 400
660 // Bad Request and that replica.Dump() returns a valid list.
662 func TestTrashHandler(t *testing.T) {
665 // Set up a REST router for testing the handlers.
666 rest := MakeRESTRouter()
668 var user_token = "USER TOKEN"
669 data_manager_token = "DATA MANAGER TOKEN"
671 good_json := []byte(`[
674 "block_mtime":1409082153
678 "block_mtime":1409082153
682 "block_mtime":1409082153
686 bad_json := []byte(`I am not a valid JSON string`)
688 type trashTest struct {
695 var testcases = []trashTest{
697 "Valid trash list from an ordinary user",
698 RequestTester{"/trash", user_token, "PUT", good_json},
699 http.StatusUnauthorized,
703 "Invalid trash list from an ordinary user",
704 RequestTester{"/trash", user_token, "PUT", bad_json},
705 http.StatusUnauthorized,
709 "Valid trash list from the data manager",
710 RequestTester{"/trash", data_manager_token, "PUT", good_json},
712 "Received 3 trash requests\n",
715 "Invalid trash list from the data manager",
716 RequestTester{"/trash", data_manager_token, "PUT", bad_json},
717 http.StatusBadRequest,
722 for _, tst := range testcases {
723 response := IssueRequest(rest, &tst.req)
724 ExpectStatusCode(t, tst.name, tst.response_code, response)
725 ExpectBody(t, tst.name, tst.response_body, response)
728 // The trash collector should have received one good list with 3
730 for i := 0; i < 3; i++ {
731 item := <-trashq.NextItem
732 if _, ok := item.(TrashRequest); !ok {
733 t.Errorf("item %v could not be parsed as a TrashRequest", item)
737 expectChannelEmpty(t, trashq.NextItem)
740 // ====================
742 // ====================
744 // IssueTestRequest executes an HTTP request described by rt, to a
745 // specified REST router. It returns the HTTP response to the request.
746 func IssueRequest(router *mux.Router, rt *RequestTester) *httptest.ResponseRecorder {
747 response := httptest.NewRecorder()
748 body := bytes.NewReader(rt.request_body)
749 req, _ := http.NewRequest(rt.method, rt.uri, body)
750 if rt.api_token != "" {
751 req.Header.Set("Authorization", "OAuth2 "+rt.api_token)
753 router.ServeHTTP(response, req)
757 // ExpectStatusCode checks whether a response has the specified status code,
758 // and reports a test failure if not.
759 func ExpectStatusCode(
763 response *httptest.ResponseRecorder) {
764 if response.Code != expected_status {
765 t.Errorf("%s: expected status %s, got %+v",
766 testname, expected_status, response)
773 expected_body string,
774 response *httptest.ResponseRecorder) {
775 if response.Body.String() != expected_body {
776 t.Errorf("%s: expected response body '%s', got %+v",
777 testname, expected_body, response)