Merge branch '15361-acr-create-workflow' refs #15361
[arvados.git] / services / keep-web / server_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bytes"
9         "crypto/md5"
10         "encoding/json"
11         "fmt"
12         "io"
13         "io/ioutil"
14         "net"
15         "net/http"
16         "os"
17         "os/exec"
18         "strings"
19         "testing"
20
21         "git.curoverse.com/arvados.git/lib/config"
22         "git.curoverse.com/arvados.git/sdk/go/arvados"
23         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
24         "git.curoverse.com/arvados.git/sdk/go/arvadostest"
25         "git.curoverse.com/arvados.git/sdk/go/keepclient"
26         check "gopkg.in/check.v1"
27 )
28
29 var testAPIHost = os.Getenv("ARVADOS_API_HOST")
30
31 var _ = check.Suite(&IntegrationSuite{})
32
33 // IntegrationSuite tests need an API server and a keep-web server
34 type IntegrationSuite struct {
35         testServer *server
36 }
37
38 func (s *IntegrationSuite) TestNoToken(c *check.C) {
39         for _, token := range []string{
40                 "",
41                 "bogustoken",
42         } {
43                 hdr, body, _ := s.runCurl(c, token, "collections.example.com", "/collections/"+arvadostest.FooCollection+"/foo")
44                 c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`)
45                 c.Check(body, check.Equals, "")
46
47                 if token != "" {
48                         hdr, body, _ = s.runCurl(c, token, "collections.example.com", "/collections/download/"+arvadostest.FooCollection+"/"+token+"/foo")
49                         c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`)
50                         c.Check(body, check.Equals, "")
51                 }
52
53                 hdr, body, _ = s.runCurl(c, token, "collections.example.com", "/bad-route")
54                 c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`)
55                 c.Check(body, check.Equals, "")
56         }
57 }
58
59 // TODO: Move most cases to functional tests -- at least use Go's own
60 // http client instead of forking curl. Just leave enough of an
61 // integration test to assure that the documented way of invoking curl
62 // really works against the server.
63 func (s *IntegrationSuite) Test404(c *check.C) {
64         for _, uri := range []string{
65                 // Routing errors (always 404 regardless of what's stored in Keep)
66                 "/foo",
67                 "/download",
68                 "/collections",
69                 "/collections/",
70                 // Implicit/generated index is not implemented yet;
71                 // until then, return 404.
72                 "/collections/" + arvadostest.FooCollection,
73                 "/collections/" + arvadostest.FooCollection + "/",
74                 "/collections/" + arvadostest.FooBarDirCollection + "/dir1",
75                 "/collections/" + arvadostest.FooBarDirCollection + "/dir1/",
76                 // Non-existent file in collection
77                 "/collections/" + arvadostest.FooCollection + "/theperthcountyconspiracy",
78                 "/collections/download/" + arvadostest.FooCollection + "/" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
79                 // Non-existent collection
80                 "/collections/" + arvadostest.NonexistentCollection,
81                 "/collections/" + arvadostest.NonexistentCollection + "/",
82                 "/collections/" + arvadostest.NonexistentCollection + "/theperthcountyconspiracy",
83                 "/collections/download/" + arvadostest.NonexistentCollection + "/" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
84         } {
85                 hdr, body, _ := s.runCurl(c, arvadostest.ActiveToken, "collections.example.com", uri)
86                 c.Check(hdr, check.Matches, "(?s)HTTP/1.1 404 Not Found\r\n.*")
87                 if len(body) > 0 {
88                         c.Check(body, check.Equals, "404 page not found\n")
89                 }
90         }
91 }
92
93 func (s *IntegrationSuite) Test1GBFile(c *check.C) {
94         if testing.Short() {
95                 c.Skip("skipping 1GB integration test in short mode")
96         }
97         s.test100BlockFile(c, 10000000)
98 }
99
100 func (s *IntegrationSuite) Test100BlockFile(c *check.C) {
101         if testing.Short() {
102                 // 3 MB
103                 s.test100BlockFile(c, 30000)
104         } else {
105                 // 300 MB
106                 s.test100BlockFile(c, 3000000)
107         }
108 }
109
110 func (s *IntegrationSuite) test100BlockFile(c *check.C, blocksize int) {
111         testdata := make([]byte, blocksize)
112         for i := 0; i < blocksize; i++ {
113                 testdata[i] = byte(' ')
114         }
115         arv, err := arvadosclient.MakeArvadosClient()
116         c.Assert(err, check.Equals, nil)
117         arv.ApiToken = arvadostest.ActiveToken
118         kc, err := keepclient.MakeKeepClient(arv)
119         c.Assert(err, check.Equals, nil)
120         loc, _, err := kc.PutB(testdata[:])
121         c.Assert(err, check.Equals, nil)
122         mtext := "."
123         for i := 0; i < 100; i++ {
124                 mtext = mtext + " " + loc
125         }
126         mtext = mtext + fmt.Sprintf(" 0:%d00:testdata.bin\n", blocksize)
127         coll := map[string]interface{}{}
128         err = arv.Create("collections",
129                 map[string]interface{}{
130                         "collection": map[string]interface{}{
131                                 "name":          fmt.Sprintf("testdata blocksize=%d", blocksize),
132                                 "manifest_text": mtext,
133                         },
134                 }, &coll)
135         c.Assert(err, check.Equals, nil)
136         uuid := coll["uuid"].(string)
137
138         hdr, body, size := s.runCurl(c, arv.ApiToken, uuid+".collections.example.com", "/testdata.bin")
139         c.Check(hdr, check.Matches, `(?s)HTTP/1.1 200 OK\r\n.*`)
140         c.Check(hdr, check.Matches, `(?si).*Content-length: `+fmt.Sprintf("%d00", blocksize)+`\r\n.*`)
141         c.Check([]byte(body)[:1234], check.DeepEquals, testdata[:1234])
142         c.Check(size, check.Equals, int64(blocksize)*100)
143 }
144
145 type curlCase struct {
146         auth    string
147         host    string
148         path    string
149         dataMD5 string
150 }
151
152 func (s *IntegrationSuite) Test200(c *check.C) {
153         s.testServer.Config.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
154         for _, spec := range []curlCase{
155                 // My collection
156                 {
157                         auth:    arvadostest.ActiveToken,
158                         host:    arvadostest.FooCollection + "--collections.example.com",
159                         path:    "/foo",
160                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
161                 },
162                 {
163                         auth:    arvadostest.ActiveToken,
164                         host:    arvadostest.FooCollection + ".collections.example.com",
165                         path:    "/foo",
166                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
167                 },
168                 {
169                         host:    strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + ".collections.example.com",
170                         path:    "/t=" + arvadostest.ActiveToken + "/foo",
171                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
172                 },
173                 {
174                         path:    "/c=" + arvadostest.FooCollectionPDH + "/t=" + arvadostest.ActiveToken + "/foo",
175                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
176                 },
177                 {
178                         path:    "/c=" + strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + "/t=" + arvadostest.ActiveToken + "/_/foo",
179                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
180                 },
181                 {
182                         path:    "/collections/download/" + arvadostest.FooCollection + "/" + arvadostest.ActiveToken + "/foo",
183                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
184                 },
185                 {
186                         auth:    "tokensobogus",
187                         path:    "/collections/download/" + arvadostest.FooCollection + "/" + arvadostest.ActiveToken + "/foo",
188                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
189                 },
190                 {
191                         auth:    arvadostest.ActiveToken,
192                         path:    "/collections/download/" + arvadostest.FooCollection + "/" + arvadostest.ActiveToken + "/foo",
193                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
194                 },
195                 {
196                         auth:    arvadostest.AnonymousToken,
197                         path:    "/collections/download/" + arvadostest.FooCollection + "/" + arvadostest.ActiveToken + "/foo",
198                         dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8",
199                 },
200
201                 // Anonymously accessible data
202                 {
203                         path:    "/c=" + arvadostest.HelloWorldCollection + "/Hello%20world.txt",
204                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
205                 },
206                 {
207                         host:    arvadostest.HelloWorldCollection + ".collections.example.com",
208                         path:    "/Hello%20world.txt",
209                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
210                 },
211                 {
212                         host:    arvadostest.HelloWorldCollection + ".collections.example.com",
213                         path:    "/_/Hello%20world.txt",
214                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
215                 },
216                 {
217                         path:    "/collections/" + arvadostest.HelloWorldCollection + "/Hello%20world.txt",
218                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
219                 },
220                 {
221                         auth:    arvadostest.ActiveToken,
222                         path:    "/collections/" + arvadostest.HelloWorldCollection + "/Hello%20world.txt",
223                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
224                 },
225                 {
226                         auth:    arvadostest.SpectatorToken,
227                         path:    "/collections/" + arvadostest.HelloWorldCollection + "/Hello%20world.txt",
228                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
229                 },
230                 {
231                         auth:    arvadostest.SpectatorToken,
232                         host:    arvadostest.HelloWorldCollection + "--collections.example.com",
233                         path:    "/Hello%20world.txt",
234                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
235                 },
236                 {
237                         auth:    arvadostest.SpectatorToken,
238                         path:    "/collections/download/" + arvadostest.HelloWorldCollection + "/" + arvadostest.SpectatorToken + "/Hello%20world.txt",
239                         dataMD5: "f0ef7081e1539ac00ef5b761b4fb01b3",
240                 },
241         } {
242                 host := spec.host
243                 if host == "" {
244                         host = "collections.example.com"
245                 }
246                 hdr, body, _ := s.runCurl(c, spec.auth, host, spec.path)
247                 c.Check(hdr, check.Matches, `(?s)HTTP/1.1 200 OK\r\n.*`)
248                 if strings.HasSuffix(spec.path, ".txt") {
249                         c.Check(hdr, check.Matches, `(?s).*\r\nContent-Type: text/plain.*`)
250                         // TODO: Check some types that aren't
251                         // automatically detected by Go's http server
252                         // by sniffing the content.
253                 }
254                 c.Check(fmt.Sprintf("%x", md5.Sum([]byte(body))), check.Equals, spec.dataMD5)
255         }
256 }
257
258 // Return header block and body.
259 func (s *IntegrationSuite) runCurl(c *check.C, token, host, uri string, args ...string) (hdr, bodyPart string, bodySize int64) {
260         curlArgs := []string{"--silent", "--show-error", "--include"}
261         testHost, testPort, _ := net.SplitHostPort(s.testServer.Addr)
262         curlArgs = append(curlArgs, "--resolve", host+":"+testPort+":"+testHost)
263         if token != "" {
264                 curlArgs = append(curlArgs, "-H", "Authorization: OAuth2 "+token)
265         }
266         curlArgs = append(curlArgs, args...)
267         curlArgs = append(curlArgs, "http://"+host+":"+testPort+uri)
268         c.Log(fmt.Sprintf("curlArgs == %#v", curlArgs))
269         cmd := exec.Command("curl", curlArgs...)
270         stdout, err := cmd.StdoutPipe()
271         c.Assert(err, check.IsNil)
272         cmd.Stderr = os.Stderr
273         err = cmd.Start()
274         c.Assert(err, check.IsNil)
275         buf := make([]byte, 2<<27)
276         n, err := io.ReadFull(stdout, buf)
277         // Discard (but measure size of) anything past 128 MiB.
278         var discarded int64
279         if err == io.ErrUnexpectedEOF {
280                 buf = buf[:n]
281         } else {
282                 c.Assert(err, check.IsNil)
283                 discarded, err = io.Copy(ioutil.Discard, stdout)
284                 c.Assert(err, check.IsNil)
285         }
286         err = cmd.Wait()
287         // Without "-f", curl exits 0 as long as it gets a valid HTTP
288         // response from the server, even if the response status
289         // indicates that the request failed. In our test suite, we
290         // always expect a valid HTTP response, and we parse the
291         // headers ourselves. If curl exits non-zero, our testing
292         // environment is broken.
293         c.Assert(err, check.Equals, nil)
294         hdrsAndBody := strings.SplitN(string(buf), "\r\n\r\n", 2)
295         c.Assert(len(hdrsAndBody), check.Equals, 2)
296         hdr = hdrsAndBody[0]
297         bodyPart = hdrsAndBody[1]
298         bodySize = int64(len(bodyPart)) + discarded
299         return
300 }
301
302 func (s *IntegrationSuite) TestMetrics(c *check.C) {
303         s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = s.testServer.Addr
304         origin := "http://" + s.testServer.Addr
305         req, _ := http.NewRequest("GET", origin+"/notfound", nil)
306         _, err := http.DefaultClient.Do(req)
307         c.Assert(err, check.IsNil)
308         req, _ = http.NewRequest("GET", origin+"/by_id/", nil)
309         req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
310         resp, err := http.DefaultClient.Do(req)
311         c.Assert(err, check.IsNil)
312         c.Check(resp.StatusCode, check.Equals, http.StatusOK)
313         for i := 0; i < 2; i++ {
314                 req, _ = http.NewRequest("GET", origin+"/foo", nil)
315                 req.Host = arvadostest.FooCollection + ".example.com"
316                 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
317                 resp, err = http.DefaultClient.Do(req)
318                 c.Assert(err, check.IsNil)
319                 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
320                 buf, _ := ioutil.ReadAll(resp.Body)
321                 c.Check(buf, check.DeepEquals, []byte("foo"))
322                 resp.Body.Close()
323         }
324
325         s.testServer.Config.Cache.updateGauges()
326
327         req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
328         resp, err = http.DefaultClient.Do(req)
329         c.Assert(err, check.IsNil)
330         c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
331
332         req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
333         req.Header.Set("Authorization", "Bearer badtoken")
334         resp, err = http.DefaultClient.Do(req)
335         c.Assert(err, check.IsNil)
336         c.Check(resp.StatusCode, check.Equals, http.StatusForbidden)
337
338         req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
339         req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
340         resp, err = http.DefaultClient.Do(req)
341         c.Assert(err, check.IsNil)
342         c.Check(resp.StatusCode, check.Equals, http.StatusOK)
343         type summary struct {
344                 SampleCount string  `json:"sample_count"`
345                 SampleSum   float64 `json:"sample_sum"`
346                 Quantile    []struct {
347                         Quantile float64
348                         Value    float64
349                 }
350         }
351         type counter struct {
352                 Value int64
353         }
354         type gauge struct {
355                 Value float64
356         }
357         var ents []struct {
358                 Name   string
359                 Help   string
360                 Type   string
361                 Metric []struct {
362                         Label []struct {
363                                 Name  string
364                                 Value string
365                         }
366                         Counter counter
367                         Gauge   gauge
368                         Summary summary
369                 }
370         }
371         json.NewDecoder(resp.Body).Decode(&ents)
372         summaries := map[string]summary{}
373         gauges := map[string]gauge{}
374         counters := map[string]counter{}
375         for _, e := range ents {
376                 for _, m := range e.Metric {
377                         labels := map[string]string{}
378                         for _, lbl := range m.Label {
379                                 labels[lbl.Name] = lbl.Value
380                         }
381                         summaries[e.Name+"/"+labels["method"]+"/"+labels["code"]] = m.Summary
382                         counters[e.Name+"/"+labels["method"]+"/"+labels["code"]] = m.Counter
383                         gauges[e.Name+"/"+labels["method"]+"/"+labels["code"]] = m.Gauge
384                 }
385         }
386         c.Check(summaries["request_duration_seconds/get/200"].SampleSum, check.Not(check.Equals), 0)
387         c.Check(summaries["request_duration_seconds/get/200"].SampleCount, check.Equals, "3")
388         c.Check(summaries["request_duration_seconds/get/404"].SampleCount, check.Equals, "1")
389         c.Check(summaries["time_to_status_seconds/get/404"].SampleCount, check.Equals, "1")
390         c.Check(counters["arvados_keepweb_collectioncache_requests//"].Value, check.Equals, int64(2))
391         c.Check(counters["arvados_keepweb_collectioncache_api_calls//"].Value, check.Equals, int64(1))
392         c.Check(counters["arvados_keepweb_collectioncache_hits//"].Value, check.Equals, int64(1))
393         c.Check(counters["arvados_keepweb_collectioncache_pdh_hits//"].Value, check.Equals, int64(1))
394         c.Check(counters["arvados_keepweb_collectioncache_permission_hits//"].Value, check.Equals, int64(1))
395         c.Check(gauges["arvados_keepweb_collectioncache_cached_manifests//"].Value, check.Equals, float64(1))
396         // FooCollection's cached manifest size is 45 ("1f4b0....+45") plus one 51-byte blob signature
397         c.Check(gauges["arvados_keepweb_collectioncache_cached_manifest_bytes//"].Value, check.Equals, float64(45+51))
398
399         // If the Host header indicates a collection, /metrics.json
400         // refers to a file in the collection -- the metrics handler
401         // must not intercept that route.
402         req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
403         req.Host = strings.Replace(arvadostest.FooCollectionPDH, "+", "-", -1) + ".example.com"
404         req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
405         resp, err = http.DefaultClient.Do(req)
406         c.Assert(err, check.IsNil)
407         c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
408 }
409
410 func (s *IntegrationSuite) SetUpSuite(c *check.C) {
411         arvadostest.StartAPI()
412         arvadostest.StartKeep(2, true)
413
414         arv, err := arvadosclient.MakeArvadosClient()
415         c.Assert(err, check.Equals, nil)
416         arv.ApiToken = arvadostest.ActiveToken
417         kc, err := keepclient.MakeKeepClient(arv)
418         c.Assert(err, check.Equals, nil)
419         kc.PutB([]byte("Hello world\n"))
420         kc.PutB([]byte("foo"))
421         kc.PutB([]byte("foobar"))
422         kc.PutB([]byte("waz"))
423 }
424
425 func (s *IntegrationSuite) TearDownSuite(c *check.C) {
426         arvadostest.StopKeep(2)
427         arvadostest.StopAPI()
428 }
429
430 func (s *IntegrationSuite) SetUpTest(c *check.C) {
431         arvadostest.ResetEnv()
432         ldr := config.NewLoader(bytes.NewBufferString("Clusters: {zzzzz: {}}"), nil)
433         ldr.Path = "-"
434         arvCfg, err := ldr.Load()
435         c.Check(err, check.IsNil)
436         cfg := newConfig(arvCfg)
437         c.Assert(err, check.IsNil)
438         cfg.Client = arvados.Client{
439                 APIHost:  testAPIHost,
440                 Insecure: true,
441         }
442         listen := "127.0.0.1:0"
443         cfg.cluster.Services.WebDAV.InternalURLs[arvados.URL{Host: listen}] = arvados.ServiceInstance{}
444         cfg.cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: listen}] = arvados.ServiceInstance{}
445         cfg.cluster.ManagementToken = arvadostest.ManagementToken
446         cfg.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
447         s.testServer = &server{Config: cfg}
448         err = s.testServer.Start()
449         c.Assert(err, check.Equals, nil)
450 }
451
452 func (s *IntegrationSuite) TearDownTest(c *check.C) {
453         var err error
454         if s.testServer != nil {
455                 err = s.testServer.Close()
456         }
457         c.Check(err, check.Equals, nil)
458 }
459
460 // Gocheck boilerplate
461 func Test(t *testing.T) {
462         check.TestingT(t)
463 }