1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
23 "git.arvados.org/arvados.git/lib/controller/dblock"
24 "git.arvados.org/arvados.git/lib/controller/rpc"
25 "git.arvados.org/arvados.git/sdk/go/arvados"
26 "git.arvados.org/arvados.git/sdk/go/arvadostest"
27 "git.arvados.org/arvados.git/sdk/go/auth"
28 "git.arvados.org/arvados.git/sdk/go/ctxlog"
29 "git.arvados.org/arvados.git/sdk/go/httpserver"
30 "github.com/prometheus/client_golang/prometheus"
31 check "gopkg.in/check.v1"
34 // Gocheck boilerplate
35 func Test(t *testing.T) {
39 var _ = check.Suite(&HandlerSuite{})
41 type HandlerSuite struct {
42 cluster *arvados.Cluster
44 railsSpy *arvadostest.Proxy
47 cancel context.CancelFunc
50 func (s *HandlerSuite) SetUpTest(c *check.C) {
51 s.logbuf = &bytes.Buffer{}
52 s.ctx, s.cancel = context.WithCancel(context.Background())
53 s.ctx = ctxlog.Context(s.ctx, ctxlog.New(io.MultiWriter(os.Stderr, s.logbuf), "json", "debug"))
54 s.cluster = &arvados.Cluster{
56 PostgreSQL: integrationTestCluster().PostgreSQL,
58 s.cluster.API.RequestTimeout = arvados.Duration(5 * time.Minute)
59 s.cluster.TLS.Insecure = true
60 arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
61 s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
62 arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, s.railsSpy.URL.String())
63 arvadostest.SetServiceURL(&s.cluster.Services.Controller, "http://localhost:/")
64 s.handler = newHandler(s.ctx, s.cluster, "", prometheus.NewRegistry()).(*Handler)
67 func (s *HandlerSuite) TearDownTest(c *check.C) {
70 // Wait for dblocks to be released. Otherwise, a subsequent
71 // test might time out waiting to acquire them.
72 timeout := time.After(10 * time.Second)
73 for _, locker := range []*dblock.DBLocker{dblock.TrashSweep, dblock.ContainerLogSweep} {
74 ok := make(chan struct{})
76 if locker.Lock(context.Background(), s.handler.dbConnector.GetDB) {
83 c.Log("timed out waiting for dblocks")
90 func (s *HandlerSuite) TestConfigExport(c *check.C) {
91 s.cluster.ManagementToken = "secret"
92 s.cluster.SystemRootToken = "secret"
93 s.cluster.Collections.BlobSigning = true
94 s.cluster.Collections.BlobSigningTTL = arvados.Duration(23 * time.Second)
95 for _, method := range []string{"GET", "OPTIONS"} {
96 req := httptest.NewRequest(method, "/arvados/v1/config", nil)
97 resp := httptest.NewRecorder()
98 s.handler.ServeHTTP(resp, req)
99 c.Log(resp.Body.String())
100 if !c.Check(resp.Code, check.Equals, http.StatusOK) {
103 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, `*`)
104 c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Matches, `.*\bGET\b.*`)
105 c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Matches, `.+`)
106 if method == "OPTIONS" {
107 c.Check(resp.Body.String(), check.HasLen, 0)
110 var cluster arvados.Cluster
111 err := json.Unmarshal(resp.Body.Bytes(), &cluster)
112 c.Check(err, check.IsNil)
113 c.Check(cluster.ManagementToken, check.Equals, "")
114 c.Check(cluster.SystemRootToken, check.Equals, "")
115 c.Check(cluster.Collections.BlobSigning, check.Equals, true)
116 c.Check(cluster.Collections.BlobSigningTTL, check.Equals, arvados.Duration(23*time.Second))
120 func (s *HandlerSuite) TestDiscoveryDocCache(c *check.C) {
121 countRailsReqs := func() int {
124 for _, req := range s.railsSpy.RequestDumps {
125 if bytes.Contains(req, []byte("/discovery/v1/apis/arvados/v1/rest")) {
131 getDD := func() int {
132 req := httptest.NewRequest(http.MethodGet, "/discovery/v1/apis/arvados/v1/rest", nil)
133 resp := httptest.NewRecorder()
134 s.handler.ServeHTTP(resp, req)
135 if resp.Code == http.StatusOK {
136 var dd arvados.DiscoveryDocument
137 err := json.Unmarshal(resp.Body.Bytes(), &dd)
138 c.Check(err, check.IsNil)
139 c.Check(dd.Schemas["Collection"].UUIDPrefix, check.Equals, "4zz18")
143 getDDConcurrently := func(n int, expectCode int, checkArgs ...interface{}) *sync.WaitGroup {
144 var wg sync.WaitGroup
145 for i := 0; i < n; i++ {
149 c.Check(getDD(), check.Equals, append([]interface{}{expectCode}, checkArgs...)...)
154 clearCache := func() {
155 for _, ent := range s.handler.cache {
156 ent.refreshLock.Lock()
158 ent.body, ent.header, ent.refreshAfter = nil, nil, time.Time{}
160 ent.refreshLock.Unlock()
163 waitPendingUpdates := func() {
164 for _, ent := range s.handler.cache {
165 ent.refreshLock.Lock()
166 defer ent.refreshLock.Unlock()
168 defer ent.mtx.Unlock()
171 refreshNow := func() {
173 for _, ent := range s.handler.cache {
174 ent.refreshAfter = time.Now()
177 expireNow := func() {
179 for _, ent := range s.handler.cache {
180 ent.expireAfter = time.Now()
184 // Easy path: first req fetches, subsequent reqs use cache.
185 c.Check(countRailsReqs(), check.Equals, 0)
186 c.Check(getDD(), check.Equals, http.StatusOK)
187 c.Check(countRailsReqs(), check.Equals, 1)
188 c.Check(getDD(), check.Equals, http.StatusOK)
189 c.Check(countRailsReqs(), check.Equals, 1)
190 c.Check(getDD(), check.Equals, http.StatusOK)
191 c.Check(countRailsReqs(), check.Equals, 1)
193 // To guarantee we have concurrent requests, we set up
194 // railsSpy to hold up the Handler's outgoing requests until
195 // we send to (or close) holdReqs.
196 holdReqs := make(chan struct{})
197 s.railsSpy.Director = func(*http.Request) {
201 // Race at startup: first req fetches, other concurrent reqs
202 // wait for the initial fetch to complete, then all return.
204 reqsBefore := countRailsReqs()
205 wg := getDDConcurrently(5, http.StatusOK, check.Commentf("race at startup"))
208 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
210 // Race after expiry: concurrent reqs return the cached data
211 // but initiate a new fetch in the background.
213 holdReqs = make(chan struct{})
214 wg = getDDConcurrently(5, http.StatusOK, check.Commentf("race after expiry"))
215 reqsBefore = countRailsReqs()
218 for deadline := time.Now().Add(time.Second); time.Now().Before(deadline) && countRailsReqs() < reqsBefore+1; {
219 time.Sleep(time.Second / 100)
221 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
223 // Configure railsSpy to return an error or bad content
224 // depending on flags.
225 var wantError, wantBadContent bool
226 s.railsSpy.Director = func(req *http.Request) {
229 req.Method = "MAKE-COFFEE"
230 } else if wantBadContent {
231 req.URL.Path = "/_health/ping"
232 req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
236 // Error at startup (empty cache) => caller gets error, and we
237 // make an upstream attempt for each incoming request because
238 // we have nothing better to return
240 wantError, wantBadContent = true, false
241 reqsBefore = countRailsReqs()
242 holdReqs = make(chan struct{})
243 wg = getDDConcurrently(5, http.StatusBadGateway, check.Commentf("error at startup"))
246 c.Check(countRailsReqs(), check.Equals, reqsBefore+5)
248 // Response status is OK but body is not a discovery document
249 wantError, wantBadContent = false, true
250 reqsBefore = countRailsReqs()
251 c.Check(getDD(), check.Equals, http.StatusBadGateway)
252 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
254 // Error condition clears => caller gets OK, cache is warmed
256 wantError, wantBadContent = false, false
257 reqsBefore = countRailsReqs()
258 getDDConcurrently(5, http.StatusOK, check.Commentf("success after errors at startup")).Wait()
259 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
261 // Error with warm cache => caller gets OK (with no attempt to
263 wantError, wantBadContent = true, false
264 reqsBefore = countRailsReqs()
265 getDDConcurrently(5, http.StatusOK, check.Commentf("error with warm cache")).Wait()
266 c.Check(countRailsReqs(), check.Equals, reqsBefore)
268 // Error with stale cache => caller gets OK with stale data
269 // while the re-fetch is attempted in the background
271 wantError, wantBadContent = true, false
272 reqsBefore = countRailsReqs()
273 holdReqs = make(chan struct{})
274 getDDConcurrently(5, http.StatusOK, check.Commentf("error with stale cache")).Wait()
276 // Only one attempt to re-fetch (holdReqs ensured the first
277 // update took long enough for the last incoming request to
279 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
282 wantError, wantBadContent = false, false
283 reqsBefore = countRailsReqs()
284 holdReqs = make(chan struct{})
285 getDDConcurrently(5, http.StatusOK, check.Commentf("refresh cache after error condition clears")).Wait()
288 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
290 // Make sure expireAfter is getting set
292 exp := s.handler.cache["/discovery/v1/apis/arvados/v1/rest"].expireAfter.Sub(time.Now())
293 c.Check(exp > cacheTTL, check.Equals, true)
294 c.Check(exp < cacheExpire, check.Equals, true)
296 // After the cache *expires* it behaves as if uninitialized:
297 // each incoming request does a new upstream request until one
300 // First check failure after expiry:
302 wantError, wantBadContent = true, false
303 reqsBefore = countRailsReqs()
304 holdReqs = make(chan struct{})
305 wg = getDDConcurrently(5, http.StatusBadGateway, check.Commentf("error after expiry"))
308 c.Check(countRailsReqs(), check.Equals, reqsBefore+5)
310 // Success after expiry:
311 wantError, wantBadContent = false, false
312 reqsBefore = countRailsReqs()
313 holdReqs = make(chan struct{})
314 wg = getDDConcurrently(5, http.StatusOK, check.Commentf("success after expiry"))
317 c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
320 func (s *HandlerSuite) TestVocabularyExport(c *check.C) {
322 "strict_tags": false,
326 "labels": [{"label": "Importance"}],
329 "labels": [{"label": "High"}]
332 "labels": [{"label": "Low"}]
338 f, err := os.CreateTemp("", "test-vocabulary-*.json")
339 c.Assert(err, check.IsNil)
340 defer os.Remove(f.Name())
341 _, err = f.WriteString(voc)
342 c.Assert(err, check.IsNil)
344 s.cluster.API.VocabularyPath = f.Name()
345 for _, method := range []string{"GET", "OPTIONS"} {
346 c.Log(c.TestName()+" ", method)
347 req := httptest.NewRequest(method, "/arvados/v1/vocabulary", nil)
348 resp := httptest.NewRecorder()
349 s.handler.ServeHTTP(resp, req)
350 c.Log(resp.Body.String())
351 if !c.Check(resp.Code, check.Equals, http.StatusOK) {
354 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, `*`)
355 c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Matches, `.*\bGET\b.*`)
356 c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Matches, `.+`)
357 if method == "OPTIONS" {
358 c.Check(resp.Body.String(), check.HasLen, 0)
361 var expectedVoc, receivedVoc *arvados.Vocabulary
362 err := json.Unmarshal([]byte(voc), &expectedVoc)
363 c.Check(err, check.IsNil)
364 err = json.Unmarshal(resp.Body.Bytes(), &receivedVoc)
365 c.Check(err, check.IsNil)
366 c.Check(receivedVoc, check.DeepEquals, expectedVoc)
370 func (s *HandlerSuite) TestVocabularyFailedCheckStatus(c *check.C) {
372 "strict_tags": false,
376 "labels": [{"label": "Importance"}],
379 "labels": [{"label": "High"}]
382 "labels": [{"label": "Low"}]
388 f, err := os.CreateTemp("", "test-vocabulary-*.json")
389 c.Assert(err, check.IsNil)
390 defer os.Remove(f.Name())
391 _, err = f.WriteString(voc)
392 c.Assert(err, check.IsNil)
394 s.cluster.API.VocabularyPath = f.Name()
396 req := httptest.NewRequest("POST", "/arvados/v1/collections",
400 "IDTAGIMPORTANCE": "Critical"
404 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
405 req.Header.Set("Content-type", "application/json")
407 resp := httptest.NewRecorder()
408 s.handler.ServeHTTP(resp, req)
409 c.Log(resp.Body.String())
410 c.Assert(resp.Code, check.Equals, http.StatusBadRequest)
411 var jresp httpserver.ErrorResponse
412 err = json.Unmarshal(resp.Body.Bytes(), &jresp)
413 c.Check(err, check.IsNil)
414 c.Assert(len(jresp.Errors), check.Equals, 1)
415 c.Check(jresp.Errors[0], check.Matches, `.*tag value.*is not valid for key.*`)
418 func (s *HandlerSuite) TestProxyDiscoveryDoc(c *check.C) {
419 req := httptest.NewRequest("GET", "/discovery/v1/apis/arvados/v1/rest", nil)
420 resp := httptest.NewRecorder()
421 s.handler.ServeHTTP(resp, req)
422 c.Check(resp.Code, check.Equals, http.StatusOK)
423 var dd arvados.DiscoveryDocument
424 err := json.Unmarshal(resp.Body.Bytes(), &dd)
425 c.Check(err, check.IsNil)
426 c.Check(dd.BlobSignatureTTL, check.Not(check.Equals), int64(0))
427 c.Check(dd.BlobSignatureTTL > 0, check.Equals, true)
428 c.Check(len(dd.Resources), check.Not(check.Equals), 0)
429 c.Check(len(dd.Schemas), check.Not(check.Equals), 0)
432 // Handler should give up and exit early if request context is
433 // cancelled due to client hangup, httpserver.HandlerWithDeadline,
435 func (s *HandlerSuite) TestRequestCancel(c *check.C) {
436 ctx, cancel := context.WithCancel(context.Background())
437 req := httptest.NewRequest("GET", "/static/login_failure", nil).WithContext(ctx)
438 resp := httptest.NewRecorder()
440 s.handler.ServeHTTP(resp, req)
441 c.Check(resp.Code, check.Equals, http.StatusBadGateway)
442 var jresp httpserver.ErrorResponse
443 err := json.Unmarshal(resp.Body.Bytes(), &jresp)
444 c.Check(err, check.IsNil)
445 c.Assert(len(jresp.Errors), check.Equals, 1)
446 c.Check(jresp.Errors[0], check.Matches, `.*context canceled`)
449 func (s *HandlerSuite) TestProxyWithoutToken(c *check.C) {
450 req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
451 resp := httptest.NewRecorder()
452 s.handler.ServeHTTP(resp, req)
453 c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
454 jresp := map[string]interface{}{}
455 err := json.Unmarshal(resp.Body.Bytes(), &jresp)
456 c.Check(err, check.IsNil)
457 c.Check(jresp["errors"], check.FitsTypeOf, []interface{}{})
460 func (s *HandlerSuite) TestProxyWithToken(c *check.C) {
461 req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
462 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
463 resp := httptest.NewRecorder()
464 s.handler.ServeHTTP(resp, req)
465 c.Check(resp.Code, check.Equals, http.StatusOK)
467 err := json.Unmarshal(resp.Body.Bytes(), &u)
468 c.Check(err, check.IsNil)
469 c.Check(u.UUID, check.Equals, arvadostest.ActiveUserUUID)
472 func (s *HandlerSuite) TestProxyWithTokenInRequestBody(c *check.C) {
473 req := httptest.NewRequest("POST", "/arvados/v1/users/current", strings.NewReader(url.Values{
475 "api_token": {arvadostest.ActiveToken},
477 req.Header.Set("Content-type", "application/x-www-form-urlencoded")
478 resp := httptest.NewRecorder()
479 s.handler.ServeHTTP(resp, req)
480 c.Check(resp.Code, check.Equals, http.StatusOK)
482 err := json.Unmarshal(resp.Body.Bytes(), &u)
483 c.Check(err, check.IsNil)
484 c.Check(u.UUID, check.Equals, arvadostest.ActiveUserUUID)
487 func (s *HandlerSuite) TestProxyNotFound(c *check.C) {
488 req := httptest.NewRequest("GET", "/arvados/v1/xyzzy", nil)
489 resp := httptest.NewRecorder()
490 s.handler.ServeHTTP(resp, req)
491 c.Check(resp.Code, check.Equals, http.StatusNotFound)
492 jresp := map[string]interface{}{}
493 err := json.Unmarshal(resp.Body.Bytes(), &jresp)
494 c.Check(err, check.IsNil)
495 c.Check(jresp["errors"], check.FitsTypeOf, []interface{}{})
498 func (s *HandlerSuite) TestLogoutGoogle(c *check.C) {
499 s.cluster.Services.Workbench2.ExternalURL = arvados.URL{Scheme: "https", Host: "wb2.example", Path: "/"}
500 s.cluster.Login.Google.Enable = true
501 s.cluster.Login.Google.ClientID = "test"
502 req := httptest.NewRequest("GET", "https://0.0.0.0:1/logout?return_to=https://wb2.example/", nil)
503 resp := httptest.NewRecorder()
504 s.handler.ServeHTTP(resp, req)
505 if !c.Check(resp.Code, check.Equals, http.StatusFound) {
506 c.Log(resp.Body.String())
508 c.Check(resp.Header().Get("Location"), check.Equals, "https://wb2.example/")
511 func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
512 c.Assert(s.handler.CheckHealth(), check.IsNil)
513 req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
514 user, ok, err := s.handler.validateAPItoken(req, arvadostest.ActiveToken)
515 c.Assert(err, check.IsNil)
516 c.Check(ok, check.Equals, true)
517 c.Check(user.Authorization.UUID, check.Equals, arvadostest.ActiveTokenUUID)
518 c.Check(user.Authorization.APIToken, check.Equals, arvadostest.ActiveToken)
519 c.Check(user.Authorization.Scopes, check.DeepEquals, []string{"all"})
520 c.Check(user.UUID, check.Equals, arvadostest.ActiveUserUUID)
523 func (s *HandlerSuite) TestValidateV2APIToken(c *check.C) {
524 c.Assert(s.handler.CheckHealth(), check.IsNil)
525 req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
526 user, ok, err := s.handler.validateAPItoken(req, arvadostest.ActiveTokenV2)
527 c.Assert(err, check.IsNil)
528 c.Check(ok, check.Equals, true)
529 c.Check(user.Authorization.UUID, check.Equals, arvadostest.ActiveTokenUUID)
530 c.Check(user.Authorization.APIToken, check.Equals, arvadostest.ActiveToken)
531 c.Check(user.Authorization.Scopes, check.DeepEquals, []string{"all"})
532 c.Check(user.UUID, check.Equals, arvadostest.ActiveUserUUID)
533 c.Check(user.Authorization.TokenV2(), check.Equals, arvadostest.ActiveTokenV2)
536 func (s *HandlerSuite) TestValidateRemoteToken(c *check.C) {
537 saltedToken, err := auth.SaltToken(arvadostest.ActiveTokenV2, "abcde")
538 c.Assert(err, check.IsNil)
539 for _, trial := range []struct {
543 {http.StatusOK, saltedToken},
544 {http.StatusUnauthorized, "bogus"},
546 req := httptest.NewRequest("GET", "https://0.0.0.0:1/arvados/v1/users/current?remote=abcde", nil)
547 req.Header.Set("Authorization", "Bearer "+trial.token)
548 resp := httptest.NewRecorder()
549 s.handler.ServeHTTP(resp, req)
550 if !c.Check(resp.Code, check.Equals, trial.code) {
551 c.Logf("HTTP %d: %s", resp.Code, resp.Body.String())
556 func (s *HandlerSuite) TestLogTokenUUID(c *check.C) {
557 req := httptest.NewRequest("GET", "https://0.0.0.0/arvados/v1/users/current", nil)
558 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
559 req = req.WithContext(s.ctx)
560 resp := httptest.NewRecorder()
561 httpserver.LogRequests(s.handler).ServeHTTP(resp, req)
562 c.Check(resp.Code, check.Equals, http.StatusOK)
563 c.Check(s.logbuf.String(), check.Matches, `(?ms).*"tokenUUIDs":\["`+strings.Split(arvadostest.ActiveTokenV2, "/")[1]+`"\].*`)
566 func (s *HandlerSuite) TestCreateAPIToken(c *check.C) {
567 c.Assert(s.handler.CheckHealth(), check.IsNil)
568 req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
569 auth, err := s.handler.createAPItoken(req, arvadostest.ActiveUserUUID, nil)
570 c.Assert(err, check.IsNil)
571 c.Check(auth.Scopes, check.DeepEquals, []string{"all"})
573 user, ok, err := s.handler.validateAPItoken(req, auth.TokenV2())
574 c.Assert(err, check.IsNil)
575 c.Check(ok, check.Equals, true)
576 c.Check(user.Authorization.UUID, check.Equals, auth.UUID)
577 c.Check(user.Authorization.APIToken, check.Equals, auth.APIToken)
578 c.Check(user.Authorization.Scopes, check.DeepEquals, []string{"all"})
579 c.Check(user.UUID, check.Equals, arvadostest.ActiveUserUUID)
580 c.Check(user.Authorization.TokenV2(), check.Equals, auth.TokenV2())
583 func (s *HandlerSuite) CheckObjectType(c *check.C, url string, token string, skippedFields map[string]bool) {
584 var proxied, direct map[string]interface{}
587 // Get collection from controller
588 req := httptest.NewRequest("GET", url, nil)
589 req.Header.Set("Authorization", "Bearer "+token)
590 resp := httptest.NewRecorder()
591 s.handler.ServeHTTP(resp, req)
592 if !c.Check(resp.Code, check.Equals, http.StatusOK,
593 check.Commentf("Wasn't able to get data from the controller at %q: %q", url, resp.Body.String())) {
596 err = json.Unmarshal(resp.Body.Bytes(), &proxied)
597 c.Check(err, check.Equals, nil)
599 // Get collection directly from RailsAPI
600 client := &http.Client{
601 Transport: &http.Transport{
602 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
605 resp2, err := client.Get(s.cluster.Services.RailsAPI.ExternalURL.String() + url + "/?api_token=" + token)
606 c.Check(err, check.Equals, nil)
607 defer resp2.Body.Close()
608 if !c.Check(resp2.StatusCode, check.Equals, http.StatusOK,
609 check.Commentf("Wasn't able to get data from the RailsAPI at %q", url)) {
612 db, err := ioutil.ReadAll(resp2.Body)
613 c.Check(err, check.Equals, nil)
614 err = json.Unmarshal(db, &direct)
615 c.Check(err, check.Equals, nil)
617 // Check that all RailsAPI provided keys exist on the controller response.
618 for k := range direct {
619 if _, ok := skippedFields[k]; ok {
621 } else if val, ok := proxied[k]; !ok {
622 c.Errorf("%s's key %q missing on controller's response.", direct["kind"], k)
623 } else if direct["kind"] == "arvados#collection" && k == "manifest_text" {
624 // Tokens differ from request to request
625 c.Check(strings.Split(val.(string), "+A")[0], check.Equals, strings.Split(direct[k].(string), "+A")[0])
627 c.Check(val, check.DeepEquals, direct[k],
628 check.Commentf("RailsAPI %s key %q's value %q differs from controller's %q.", direct["kind"], k, direct[k], val))
633 func (s *HandlerSuite) TestGetObjects(c *check.C) {
634 // Get the 1st keep service's uuid from the running test server.
635 req := httptest.NewRequest("GET", "/arvados/v1/keep_services/", nil)
636 req.Header.Set("Authorization", "Bearer "+arvadostest.AdminToken)
637 resp := httptest.NewRecorder()
638 s.handler.ServeHTTP(resp, req)
639 c.Assert(resp.Code, check.Equals, http.StatusOK)
640 var ksList arvados.KeepServiceList
641 json.Unmarshal(resp.Body.Bytes(), &ksList)
642 c.Assert(len(ksList.Items), check.Not(check.Equals), 0)
643 ksUUID := ksList.Items[0].UUID
644 // Create a new token for the test user so that we're not comparing
645 // the ones from the fixtures.
646 req = httptest.NewRequest("POST", "/arvados/v1/api_client_authorizations",
648 "api_client_authorization": {
649 "owner_uuid": "`+arvadostest.AdminUserUUID+`",
650 "created_by_ip_address": "::1",
651 "last_used_by_ip_address": "::1"
654 req.Header.Set("Authorization", "Bearer "+arvadostest.SystemRootToken)
655 req.Header.Set("Content-type", "application/json")
656 resp = httptest.NewRecorder()
657 s.handler.ServeHTTP(resp, req)
658 c.Assert(resp.Code, check.Equals, http.StatusOK,
659 check.Commentf("%s", resp.Body.String()))
660 var auth arvados.APIClientAuthorization
661 json.Unmarshal(resp.Body.Bytes(), &auth)
662 c.Assert(auth.UUID, check.Not(check.Equals), "")
664 testCases := map[string]map[string]bool{
665 "api_clients/" + arvadostest.TrustedWorkbenchAPIClientUUID: nil,
666 "api_client_authorizations/" + auth.UUID: {"href": true, "modified_by_client_uuid": true, "modified_by_user_uuid": true},
667 "authorized_keys/" + arvadostest.AdminAuthorizedKeysUUID: {"href": true},
668 "collections/" + arvadostest.CollectionWithUniqueWordsUUID: {"href": true},
669 "containers/" + arvadostest.RunningContainerUUID: nil,
670 "container_requests/" + arvadostest.QueuedContainerRequestUUID: nil,
671 "groups/" + arvadostest.AProjectUUID: nil,
672 "keep_services/" + ksUUID: nil,
673 "links/" + arvadostest.ActiveUserCanReadAllUsersLinkUUID: nil,
674 "logs/" + arvadostest.CrunchstatForRunningContainerLogUUID: nil,
675 "users/" + arvadostest.ActiveUserUUID: {"href": true},
676 "virtual_machines/" + arvadostest.TestVMUUID: nil,
677 "workflows/" + arvadostest.WorkflowWithDefinitionYAMLUUID: nil,
679 for url, skippedFields := range testCases {
680 c.Logf("Testing %q", url)
681 s.CheckObjectType(c, "/arvados/v1/"+url, auth.TokenV2(), skippedFields)
685 func (s *HandlerSuite) TestRedactRailsAPIHostFromErrors(c *check.C) {
686 req := httptest.NewRequest("GET", "https://0.0.0.0:1/arvados/v1/collections/zzzzz-4zz18-abcdefghijklmno", nil)
687 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
688 resp := httptest.NewRecorder()
689 s.handler.ServeHTTP(resp, req)
690 c.Check(resp.Code, check.Equals, http.StatusNotFound)
694 c.Log(resp.Body.String())
695 c.Assert(json.NewDecoder(resp.Body).Decode(&jresp), check.IsNil)
696 c.Assert(jresp.Errors, check.HasLen, 1)
697 c.Check(jresp.Errors[0], check.Matches, `.*//railsapi\.internal/arvados/v1/collections/.*: 404 Not Found.*`)
698 c.Check(jresp.Errors[0], check.Not(check.Matches), `(?ms).*127.0.0.1.*`)
701 func (s *HandlerSuite) TestTrashSweep(c *check.C) {
702 s.cluster.SystemRootToken = arvadostest.SystemRootToken
703 s.cluster.Collections.TrashSweepInterval = arvados.Duration(time.Second / 10)
704 s.handler.CheckHealth()
705 ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
706 coll, err := s.handler.federation.CollectionCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{"name": "test trash sweep"}, EnsureUniqueName: true})
707 c.Assert(err, check.IsNil)
708 defer s.handler.federation.CollectionDelete(ctx, arvados.DeleteOptions{UUID: coll.UUID})
709 db, err := s.handler.dbConnector.GetDB(s.ctx)
710 c.Assert(err, check.IsNil)
711 _, err = db.ExecContext(s.ctx, `update collections set trash_at = $1, delete_at = $2 where uuid = $3`, time.Now().UTC().Add(time.Second/10), time.Now().UTC().Add(time.Hour), coll.UUID)
712 c.Assert(err, check.IsNil)
713 deadline := time.Now().Add(5 * time.Second)
715 if time.Now().After(deadline) {
719 updated, err := s.handler.federation.CollectionGet(ctx, arvados.GetOptions{UUID: coll.UUID, IncludeTrash: true})
720 c.Assert(err, check.IsNil)
721 if updated.IsTrashed {
724 time.Sleep(time.Second / 10)
728 func (s *HandlerSuite) TestContainerLogSweep(c *check.C) {
729 s.cluster.SystemRootToken = arvadostest.SystemRootToken
730 s.cluster.Collections.TrashSweepInterval = arvados.Duration(2 * time.Second)
731 s.handler.CheckHealth()
732 ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
733 logentry, err := s.handler.federation.LogCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
734 "object_uuid": arvadostest.CompletedContainerUUID,
735 "event_type": "stderr",
736 "properties": map[string]interface{}{
737 "text": "test container log sweep\n",
740 c.Assert(err, check.IsNil)
741 defer s.handler.federation.LogDelete(ctx, arvados.DeleteOptions{UUID: logentry.UUID})
742 deadline := time.Now().Add(5 * time.Second)
744 if time.Now().After(deadline) {
748 logentries, err := s.handler.federation.LogList(ctx, arvados.ListOptions{Filters: []arvados.Filter{{"uuid", "=", logentry.UUID}}, Limit: -1})
749 c.Assert(err, check.IsNil)
750 if len(logentries.Items) == 0 {
753 time.Sleep(time.Second / 10)
757 func (s *HandlerSuite) TestLogActivity(c *check.C) {
758 s.cluster.SystemRootToken = arvadostest.SystemRootToken
759 s.cluster.Users.ActivityLoggingPeriod = arvados.Duration(24 * time.Hour)
760 s.handler.CheckHealth()
762 testServer := newServerFromIntegrationTestEnv(c)
763 testServer.Server.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(s.handler))
764 c.Assert(testServer.Start(), check.IsNil)
765 defer testServer.Close()
767 u, _ := url.Parse("http://" + testServer.Addr)
768 client := rpc.NewConn(s.cluster.ClusterID, u, true, rpc.PassthroughTokenProvider)
770 starttime := time.Now()
771 for i := 0; i < 4; i++ {
772 for _, token := range []string{
773 arvadostest.ActiveTokenV2,
774 arvadostest.ActiveToken,
775 arvadostest.SpectatorToken,
777 ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{token}})
778 _, err := client.CollectionList(ctx, arvados.ListOptions{})
779 c.Assert(err, check.IsNil)
782 db, err := s.handler.dbConnector.GetDB(s.ctx)
783 c.Assert(err, check.IsNil)
784 for _, userUUID := range []string{arvadostest.ActiveUserUUID, arvadostest.SpectatorUserUUID} {
786 err = db.QueryRowContext(s.ctx, `select count(uuid) from logs where object_uuid = $1 and event_at > $2`, arvadostest.ActiveUserUUID, starttime.UTC()).Scan(&rows)
787 c.Assert(err, check.IsNil)
788 c.Check(rows, check.Equals, 1, check.Commentf("expect 1 row for user uuid %s", userUUID))