4869: Keepstore now returns Content-Length headers, and logs the error message
[arvados.git] / services / keepstore / handler_test.go
1 // Tests for Keep HTTP handlers:
2 //
3 //     GetBlockHandler
4 //     PutBlockHandler
5 //     IndexHandler
6 //
7 // The HTTP handlers are responsible for enforcing permission policy,
8 // so these tests must exercise all possible permission permutations.
9
10 package main
11
12 import (
13         "bytes"
14         "encoding/json"
15         "fmt"
16         "net/http"
17         "net/http/httptest"
18         "os"
19         "regexp"
20         "strings"
21         "testing"
22         "time"
23 )
24
25 // A RequestTester represents the parameters for an HTTP request to
26 // be issued on behalf of a unit test.
27 type RequestTester struct {
28         uri          string
29         api_token    string
30         method       string
31         request_body []byte
32 }
33
34 // Test GetBlockHandler on the following situations:
35 //   - permissions off, unauthenticated request, unsigned locator
36 //   - permissions on, authenticated request, signed locator
37 //   - permissions on, authenticated request, unsigned locator
38 //   - permissions on, unauthenticated request, signed locator
39 //   - permissions on, authenticated request, expired locator
40 //
41 func TestGetHandler(t *testing.T) {
42         defer teardown()
43
44         // Prepare two test Keep volumes. Our block is stored on the second volume.
45         KeepVM = MakeTestVolumeManager(2)
46         defer KeepVM.Quit()
47
48         vols := KeepVM.Volumes()
49         if err := vols[0].Put(TEST_HASH, TEST_BLOCK); err != nil {
50                 t.Error(err)
51         }
52
53         // Create locators for testing.
54         // Turn on permission settings so we can generate signed locators.
55         enforce_permissions = true
56         PermissionSecret = []byte(known_key)
57         permission_ttl = time.Duration(300) * time.Second
58
59         var (
60                 unsigned_locator  = "/" + TEST_HASH
61                 valid_timestamp   = time.Now().Add(permission_ttl)
62                 expired_timestamp = time.Now().Add(-time.Hour)
63                 signed_locator    = "/" + SignLocator(TEST_HASH, known_token, valid_timestamp)
64                 expired_locator   = "/" + SignLocator(TEST_HASH, known_token, expired_timestamp)
65         )
66
67         // -----------------
68         // Test unauthenticated request with permissions off.
69         enforce_permissions = false
70
71         // Unauthenticated request, unsigned locator
72         // => OK
73         response := IssueRequest(
74                 &RequestTester{
75                         method: "GET",
76                         uri:    unsigned_locator,
77                 })
78         ExpectStatusCode(t,
79                 "Unauthenticated request, unsigned locator", http.StatusOK, response)
80         ExpectBody(t,
81                 "Unauthenticated request, unsigned locator",
82                 string(TEST_BLOCK),
83                 response)
84
85         received_cl := response.Header().Get("Content-Length")
86         expected_cl := fmt.Sprintf("%d", len(TEST_BLOCK))
87         if received_cl != expected_cl {
88                 t.Errorf("expected Content-Length %s, got %s", expected_cl, received_cl)
89         }
90
91         received_xbs := response.Header().Get("X-Block-Size")
92         expected_xbs := fmt.Sprintf("%d", len(TEST_BLOCK))
93         if received_xbs != expected_xbs {
94                 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
95         }
96
97         // ----------------
98         // Permissions: on.
99         enforce_permissions = true
100
101         // Authenticated request, signed locator
102         // => OK
103         response = IssueRequest(&RequestTester{
104                 method:    "GET",
105                 uri:       signed_locator,
106                 api_token: known_token,
107         })
108         ExpectStatusCode(t,
109                 "Authenticated request, signed locator", http.StatusOK, response)
110         ExpectBody(t,
111                 "Authenticated request, signed locator", string(TEST_BLOCK), response)
112
113         received_xbs = response.Header().Get("X-Block-Size")
114         expected_xbs = fmt.Sprintf("%d", len(TEST_BLOCK))
115         if received_xbs != expected_xbs {
116                 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
117         }
118
119         received_cl = response.Header().Get("Content-Length")
120         expected_cl = fmt.Sprintf("%d", len(TEST_BLOCK))
121         if received_cl != expected_cl {
122                 t.Errorf("expected Content-Length %s, got %s", expected_cl, received_cl)
123         }
124
125         // Authenticated request, unsigned locator
126         // => PermissionError
127         response = IssueRequest(&RequestTester{
128                 method:    "GET",
129                 uri:       unsigned_locator,
130                 api_token: known_token,
131         })
132         ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response)
133
134         // Unauthenticated request, signed locator
135         // => PermissionError
136         response = IssueRequest(&RequestTester{
137                 method: "GET",
138                 uri:    signed_locator,
139         })
140         ExpectStatusCode(t,
141                 "Unauthenticated request, signed locator",
142                 PermissionError.HTTPCode, response)
143
144         // Authenticated request, expired locator
145         // => ExpiredError
146         response = IssueRequest(&RequestTester{
147                 method:    "GET",
148                 uri:       expired_locator,
149                 api_token: known_token,
150         })
151         ExpectStatusCode(t,
152                 "Authenticated request, expired locator",
153                 ExpiredError.HTTPCode, response)
154 }
155
156 // Test PutBlockHandler on the following situations:
157 //   - no server key
158 //   - with server key, authenticated request, unsigned locator
159 //   - with server key, unauthenticated request, unsigned locator
160 //
161 func TestPutHandler(t *testing.T) {
162         defer teardown()
163
164         // Prepare two test Keep volumes.
165         KeepVM = MakeTestVolumeManager(2)
166         defer KeepVM.Quit()
167
168         // --------------
169         // No server key.
170
171         // Unauthenticated request, no server key
172         // => OK (unsigned response)
173         unsigned_locator := "/" + TEST_HASH
174         response := IssueRequest(
175                 &RequestTester{
176                         method:       "PUT",
177                         uri:          unsigned_locator,
178                         request_body: TEST_BLOCK,
179                 })
180
181         ExpectStatusCode(t,
182                 "Unauthenticated request, no server key", http.StatusOK, response)
183         ExpectBody(t,
184                 "Unauthenticated request, no server key",
185                 TEST_HASH_PUT_RESPONSE, response)
186
187         // ------------------
188         // With a server key.
189
190         PermissionSecret = []byte(known_key)
191         permission_ttl = time.Duration(300) * time.Second
192
193         // When a permission key is available, the locator returned
194         // from an authenticated PUT request will be signed.
195
196         // Authenticated PUT, signed locator
197         // => OK (signed response)
198         response = IssueRequest(
199                 &RequestTester{
200                         method:       "PUT",
201                         uri:          unsigned_locator,
202                         request_body: TEST_BLOCK,
203                         api_token:    known_token,
204                 })
205
206         ExpectStatusCode(t,
207                 "Authenticated PUT, signed locator, with server key",
208                 http.StatusOK, response)
209         response_locator := strings.TrimSpace(response.Body.String())
210         if !VerifySignature(response_locator, known_token) {
211                 t.Errorf("Authenticated PUT, signed locator, with server key:\n"+
212                         "response '%s' does not contain a valid signature",
213                         response_locator)
214         }
215
216         // Unauthenticated PUT, unsigned locator
217         // => OK
218         response = IssueRequest(
219                 &RequestTester{
220                         method:       "PUT",
221                         uri:          unsigned_locator,
222                         request_body: TEST_BLOCK,
223                 })
224
225         ExpectStatusCode(t,
226                 "Unauthenticated PUT, unsigned locator, with server key",
227                 http.StatusOK, response)
228         ExpectBody(t,
229                 "Unauthenticated PUT, unsigned locator, with server key",
230                 TEST_HASH_PUT_RESPONSE, response)
231 }
232
233 // Test /index requests:
234 //   - unauthenticated /index request
235 //   - unauthenticated /index/prefix request
236 //   - authenticated   /index request        | non-superuser
237 //   - authenticated   /index/prefix request | non-superuser
238 //   - authenticated   /index request        | superuser
239 //   - authenticated   /index/prefix request | superuser
240 //
241 // The only /index requests that should succeed are those issued by the
242 // superuser. They should pass regardless of the value of enforce_permissions.
243 //
244 func TestIndexHandler(t *testing.T) {
245         defer teardown()
246
247         // Set up Keep volumes and populate them.
248         // Include multiple blocks on different volumes, and
249         // some metadata files (which should be omitted from index listings)
250         KeepVM = MakeTestVolumeManager(2)
251         defer KeepVM.Quit()
252
253         vols := KeepVM.Volumes()
254         vols[0].Put(TEST_HASH, TEST_BLOCK)
255         vols[1].Put(TEST_HASH_2, TEST_BLOCK_2)
256         vols[0].Put(TEST_HASH+".meta", []byte("metadata"))
257         vols[1].Put(TEST_HASH_2+".meta", []byte("metadata"))
258
259         data_manager_token = "DATA MANAGER TOKEN"
260
261         unauthenticated_req := &RequestTester{
262                 method: "GET",
263                 uri:    "/index",
264         }
265         authenticated_req := &RequestTester{
266                 method:    "GET",
267                 uri:       "/index",
268                 api_token: known_token,
269         }
270         superuser_req := &RequestTester{
271                 method:    "GET",
272                 uri:       "/index",
273                 api_token: data_manager_token,
274         }
275         unauth_prefix_req := &RequestTester{
276                 method: "GET",
277                 uri:    "/index/" + TEST_HASH[0:3],
278         }
279         auth_prefix_req := &RequestTester{
280                 method:    "GET",
281                 uri:       "/index/" + TEST_HASH[0:3],
282                 api_token: known_token,
283         }
284         superuser_prefix_req := &RequestTester{
285                 method:    "GET",
286                 uri:       "/index/" + TEST_HASH[0:3],
287                 api_token: data_manager_token,
288         }
289
290         // -------------------------------------------------------------
291         // Only the superuser should be allowed to issue /index requests.
292
293         // ---------------------------
294         // enforce_permissions enabled
295         // This setting should not affect tests passing.
296         enforce_permissions = true
297
298         // unauthenticated /index request
299         // => UnauthorizedError
300         response := IssueRequest(unauthenticated_req)
301         ExpectStatusCode(t,
302                 "enforce_permissions on, unauthenticated request",
303                 UnauthorizedError.HTTPCode,
304                 response)
305
306         // unauthenticated /index/prefix request
307         // => UnauthorizedError
308         response = IssueRequest(unauth_prefix_req)
309         ExpectStatusCode(t,
310                 "permissions on, unauthenticated /index/prefix request",
311                 UnauthorizedError.HTTPCode,
312                 response)
313
314         // authenticated /index request, non-superuser
315         // => UnauthorizedError
316         response = IssueRequest(authenticated_req)
317         ExpectStatusCode(t,
318                 "permissions on, authenticated request, non-superuser",
319                 UnauthorizedError.HTTPCode,
320                 response)
321
322         // authenticated /index/prefix request, non-superuser
323         // => UnauthorizedError
324         response = IssueRequest(auth_prefix_req)
325         ExpectStatusCode(t,
326                 "permissions on, authenticated /index/prefix request, non-superuser",
327                 UnauthorizedError.HTTPCode,
328                 response)
329
330         // superuser /index request
331         // => OK
332         response = IssueRequest(superuser_req)
333         ExpectStatusCode(t,
334                 "permissions on, superuser request",
335                 http.StatusOK,
336                 response)
337
338         // ----------------------------
339         // enforce_permissions disabled
340         // Valid Request should still pass.
341         enforce_permissions = false
342
343         // superuser /index request
344         // => OK
345         response = IssueRequest(superuser_req)
346         ExpectStatusCode(t,
347                 "permissions on, superuser request",
348                 http.StatusOK,
349                 response)
350
351         expected := `^` + TEST_HASH + `\+\d+ \d+\n` +
352                 TEST_HASH_2 + `\+\d+ \d+\n$`
353         match, _ := regexp.MatchString(expected, response.Body.String())
354         if !match {
355                 t.Errorf(
356                         "permissions on, superuser request: expected %s, got:\n%s",
357                         expected, response.Body.String())
358         }
359
360         // superuser /index/prefix request
361         // => OK
362         response = IssueRequest(superuser_prefix_req)
363         ExpectStatusCode(t,
364                 "permissions on, superuser request",
365                 http.StatusOK,
366                 response)
367
368         expected = `^` + TEST_HASH + `\+\d+ \d+\n$`
369         match, _ = regexp.MatchString(expected, response.Body.String())
370         if !match {
371                 t.Errorf(
372                         "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
373                         expected, response.Body.String())
374         }
375 }
376
377 // TestDeleteHandler
378 //
379 // Cases tested:
380 //
381 //   With no token and with a non-data-manager token:
382 //   * Delete existing block
383 //     (test for 403 Forbidden, confirm block not deleted)
384 //
385 //   With data manager token:
386 //
387 //   * Delete existing block
388 //     (test for 200 OK, response counts, confirm block deleted)
389 //
390 //   * Delete nonexistent block
391 //     (test for 200 OK, response counts)
392 //
393 //   TODO(twp):
394 //
395 //   * Delete block on read-only and read-write volume
396 //     (test for 200 OK, response with copies_deleted=1,
397 //     copies_failed=1, confirm block deleted only on r/w volume)
398 //
399 //   * Delete block on read-only volume only
400 //     (test for 200 OK, response with copies_deleted=0, copies_failed=1,
401 //     confirm block not deleted)
402 //
403 func TestDeleteHandler(t *testing.T) {
404         defer teardown()
405
406         // Set up Keep volumes and populate them.
407         // Include multiple blocks on different volumes, and
408         // some metadata files (which should be omitted from index listings)
409         KeepVM = MakeTestVolumeManager(2)
410         defer KeepVM.Quit()
411
412         vols := KeepVM.Volumes()
413         vols[0].Put(TEST_HASH, TEST_BLOCK)
414
415         // Explicitly set the permission_ttl to 0 for these
416         // tests, to ensure the MockVolume deletes the blocks
417         // even though they have just been created.
418         permission_ttl = time.Duration(0)
419
420         var user_token = "NOT DATA MANAGER TOKEN"
421         data_manager_token = "DATA MANAGER TOKEN"
422
423         unauth_req := &RequestTester{
424                 method: "DELETE",
425                 uri:    "/" + TEST_HASH,
426         }
427
428         user_req := &RequestTester{
429                 method:    "DELETE",
430                 uri:       "/" + TEST_HASH,
431                 api_token: user_token,
432         }
433
434         superuser_existing_block_req := &RequestTester{
435                 method:    "DELETE",
436                 uri:       "/" + TEST_HASH,
437                 api_token: data_manager_token,
438         }
439
440         superuser_nonexistent_block_req := &RequestTester{
441                 method:    "DELETE",
442                 uri:       "/" + TEST_HASH_2,
443                 api_token: data_manager_token,
444         }
445
446         // Unauthenticated request returns PermissionError.
447         var response *httptest.ResponseRecorder
448         response = IssueRequest(unauth_req)
449         ExpectStatusCode(t,
450                 "unauthenticated request",
451                 PermissionError.HTTPCode,
452                 response)
453
454         // Authenticated non-admin request returns PermissionError.
455         response = IssueRequest(user_req)
456         ExpectStatusCode(t,
457                 "authenticated non-admin request",
458                 PermissionError.HTTPCode,
459                 response)
460
461         // Authenticated admin request for nonexistent block.
462         type deletecounter struct {
463                 Deleted int `json:"copies_deleted"`
464                 Failed  int `json:"copies_failed"`
465         }
466         var response_dc, expected_dc deletecounter
467
468         response = IssueRequest(superuser_nonexistent_block_req)
469         ExpectStatusCode(t,
470                 "data manager request, nonexistent block",
471                 http.StatusNotFound,
472                 response)
473
474         // Authenticated admin request for existing block while never_delete is set.
475         never_delete = true
476         response = IssueRequest(superuser_existing_block_req)
477         ExpectStatusCode(t,
478                 "authenticated request, existing block, method disabled",
479                 MethodDisabledError.HTTPCode,
480                 response)
481         never_delete = false
482
483         // Authenticated admin request for existing block.
484         response = IssueRequest(superuser_existing_block_req)
485         ExpectStatusCode(t,
486                 "data manager request, existing block",
487                 http.StatusOK,
488                 response)
489         // Expect response {"copies_deleted":1,"copies_failed":0}
490         expected_dc = deletecounter{1, 0}
491         json.NewDecoder(response.Body).Decode(&response_dc)
492         if response_dc != expected_dc {
493                 t.Errorf("superuser_existing_block_req\nexpected: %+v\nreceived: %+v",
494                         expected_dc, response_dc)
495         }
496         // Confirm the block has been deleted
497         _, err := vols[0].Get(TEST_HASH)
498         var block_deleted = os.IsNotExist(err)
499         if !block_deleted {
500                 t.Error("superuser_existing_block_req: block not deleted")
501         }
502
503         // A DELETE request on a block newer than permission_ttl should return
504         // success but leave the block on the volume.
505         vols[0].Put(TEST_HASH, TEST_BLOCK)
506         permission_ttl = time.Duration(1) * time.Hour
507
508         response = IssueRequest(superuser_existing_block_req)
509         ExpectStatusCode(t,
510                 "data manager request, existing block",
511                 http.StatusOK,
512                 response)
513         // Expect response {"copies_deleted":1,"copies_failed":0}
514         expected_dc = deletecounter{1, 0}
515         json.NewDecoder(response.Body).Decode(&response_dc)
516         if response_dc != expected_dc {
517                 t.Errorf("superuser_existing_block_req\nexpected: %+v\nreceived: %+v",
518                         expected_dc, response_dc)
519         }
520         // Confirm the block has NOT been deleted.
521         _, err = vols[0].Get(TEST_HASH)
522         if err != nil {
523                 t.Errorf("testing delete on new block: %s\n", err)
524         }
525 }
526
527 // TestPullHandler
528 //
529 // Test handling of the PUT /pull statement.
530 //
531 // Cases tested: syntactically valid and invalid pull lists, from the
532 // data manager and from unprivileged users:
533 //
534 //   1. Valid pull list from an ordinary user
535 //      (expected result: 401 Unauthorized)
536 //
537 //   2. Invalid pull request from an ordinary user
538 //      (expected result: 401 Unauthorized)
539 //
540 //   3. Valid pull request from the data manager
541 //      (expected result: 200 OK with request body "Received 3 pull
542 //      requests"
543 //
544 //   4. Invalid pull request from the data manager
545 //      (expected result: 400 Bad Request)
546 //
547 // Test that in the end, the pull manager received a good pull list with
548 // the expected number of requests.
549 //
550 // TODO(twp): test concurrency: launch 100 goroutines to update the
551 // pull list simultaneously.  Make sure that none of them return 400
552 // Bad Request and that pullq.GetList() returns a valid list.
553 //
554 func TestPullHandler(t *testing.T) {
555         defer teardown()
556
557         var user_token = "USER TOKEN"
558         data_manager_token = "DATA MANAGER TOKEN"
559
560         good_json := []byte(`[
561                 {
562                         "locator":"locator_with_two_servers",
563                         "servers":[
564                                 "server1",
565                                 "server2"
566                         ]
567                 },
568                 {
569                         "locator":"locator_with_no_servers",
570                         "servers":[]
571                 },
572                 {
573                         "locator":"",
574                         "servers":["empty_locator"]
575                 }
576         ]`)
577
578         bad_json := []byte(`{ "key":"I'm a little teapot" }`)
579
580         type pullTest struct {
581                 name          string
582                 req           RequestTester
583                 response_code int
584                 response_body string
585         }
586         var testcases = []pullTest{
587                 {
588                         "Valid pull list from an ordinary user",
589                         RequestTester{"/pull", user_token, "PUT", good_json},
590                         http.StatusUnauthorized,
591                         "Unauthorized\n",
592                 },
593                 {
594                         "Invalid pull request from an ordinary user",
595                         RequestTester{"/pull", user_token, "PUT", bad_json},
596                         http.StatusUnauthorized,
597                         "Unauthorized\n",
598                 },
599                 {
600                         "Valid pull request from the data manager",
601                         RequestTester{"/pull", data_manager_token, "PUT", good_json},
602                         http.StatusOK,
603                         "Received 3 pull requests\n",
604                 },
605                 {
606                         "Invalid pull request from the data manager",
607                         RequestTester{"/pull", data_manager_token, "PUT", bad_json},
608                         http.StatusBadRequest,
609                         "Bad Request\n",
610                 },
611         }
612
613         for _, tst := range testcases {
614                 response := IssueRequest(&tst.req)
615                 ExpectStatusCode(t, tst.name, tst.response_code, response)
616                 ExpectBody(t, tst.name, tst.response_body, response)
617         }
618
619         // The Keep pull manager should have received one good list with 3
620         // requests on it.
621         for i := 0; i < 3; i++ {
622                 item := <-pullq.NextItem
623                 if _, ok := item.(PullRequest); !ok {
624                         t.Errorf("item %v could not be parsed as a PullRequest", item)
625                 }
626         }
627
628         expectChannelEmpty(t, pullq.NextItem)
629 }
630
631 // TestTrashHandler
632 //
633 // Test cases:
634 //
635 // Cases tested: syntactically valid and invalid trash lists, from the
636 // data manager and from unprivileged users:
637 //
638 //   1. Valid trash list from an ordinary user
639 //      (expected result: 401 Unauthorized)
640 //
641 //   2. Invalid trash list from an ordinary user
642 //      (expected result: 401 Unauthorized)
643 //
644 //   3. Valid trash list from the data manager
645 //      (expected result: 200 OK with request body "Received 3 trash
646 //      requests"
647 //
648 //   4. Invalid trash list from the data manager
649 //      (expected result: 400 Bad Request)
650 //
651 // Test that in the end, the trash collector received a good list
652 // trash list with the expected number of requests.
653 //
654 // TODO(twp): test concurrency: launch 100 goroutines to update the
655 // pull list simultaneously.  Make sure that none of them return 400
656 // Bad Request and that replica.Dump() returns a valid list.
657 //
658 func TestTrashHandler(t *testing.T) {
659         defer teardown()
660
661         var user_token = "USER TOKEN"
662         data_manager_token = "DATA MANAGER TOKEN"
663
664         good_json := []byte(`[
665                 {
666                         "locator":"block1",
667                         "block_mtime":1409082153
668                 },
669                 {
670                         "locator":"block2",
671                         "block_mtime":1409082153
672                 },
673                 {
674                         "locator":"block3",
675                         "block_mtime":1409082153
676                 }
677         ]`)
678
679         bad_json := []byte(`I am not a valid JSON string`)
680
681         type trashTest struct {
682                 name          string
683                 req           RequestTester
684                 response_code int
685                 response_body string
686         }
687
688         var testcases = []trashTest{
689                 {
690                         "Valid trash list from an ordinary user",
691                         RequestTester{"/trash", user_token, "PUT", good_json},
692                         http.StatusUnauthorized,
693                         "Unauthorized\n",
694                 },
695                 {
696                         "Invalid trash list from an ordinary user",
697                         RequestTester{"/trash", user_token, "PUT", bad_json},
698                         http.StatusUnauthorized,
699                         "Unauthorized\n",
700                 },
701                 {
702                         "Valid trash list from the data manager",
703                         RequestTester{"/trash", data_manager_token, "PUT", good_json},
704                         http.StatusOK,
705                         "Received 3 trash requests\n",
706                 },
707                 {
708                         "Invalid trash list from the data manager",
709                         RequestTester{"/trash", data_manager_token, "PUT", bad_json},
710                         http.StatusBadRequest,
711                         "Bad Request\n",
712                 },
713         }
714
715         for _, tst := range testcases {
716                 response := IssueRequest(&tst.req)
717                 ExpectStatusCode(t, tst.name, tst.response_code, response)
718                 ExpectBody(t, tst.name, tst.response_body, response)
719         }
720
721         // The trash collector should have received one good list with 3
722         // requests on it.
723         for i := 0; i < 3; i++ {
724                 item := <-trashq.NextItem
725                 if _, ok := item.(TrashRequest); !ok {
726                         t.Errorf("item %v could not be parsed as a TrashRequest", item)
727                 }
728         }
729
730         expectChannelEmpty(t, trashq.NextItem)
731 }
732
733 // ====================
734 // Helper functions
735 // ====================
736
737 // IssueTestRequest executes an HTTP request described by rt, to a
738 // REST router.  It returns the HTTP response to the request.
739 func IssueRequest(rt *RequestTester) *httptest.ResponseRecorder {
740         response := httptest.NewRecorder()
741         body := bytes.NewReader(rt.request_body)
742         req, _ := http.NewRequest(rt.method, rt.uri, body)
743         if rt.api_token != "" {
744                 req.Header.Set("Authorization", "OAuth2 "+rt.api_token)
745         }
746         loggingRouter := MakeLoggingRESTRouter()
747         loggingRouter.ServeHTTP(response, req)
748         return response
749 }
750
751 // ExpectStatusCode checks whether a response has the specified status code,
752 // and reports a test failure if not.
753 func ExpectStatusCode(
754         t *testing.T,
755         testname string,
756         expected_status int,
757         response *httptest.ResponseRecorder) {
758         if response.Code != expected_status {
759                 t.Errorf("%s: expected status %s, got %+v",
760                         testname, expected_status, response)
761         }
762 }
763
764 func ExpectBody(
765         t *testing.T,
766         testname string,
767         expected_body string,
768         response *httptest.ResponseRecorder) {
769         if response.Body.String() != expected_body {
770                 t.Errorf("%s: expected response body '%s', got %+v",
771                         testname, expected_body, response)
772         }
773 }