20862: Merge branch 'main' into 20862-google-api-client
[arvados.git] / services / ws / service_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package ws
6
7 import (
8         "bytes"
9         "context"
10         "flag"
11         "io/ioutil"
12         "net/http"
13         "net/http/httptest"
14         "os"
15         "strings"
16         "sync"
17         "time"
18
19         "git.arvados.org/arvados.git/lib/config"
20         "git.arvados.org/arvados.git/lib/service"
21         "git.arvados.org/arvados.git/sdk/go/arvados"
22         "git.arvados.org/arvados.git/sdk/go/arvadostest"
23         "git.arvados.org/arvados.git/sdk/go/ctxlog"
24         "git.arvados.org/arvados.git/sdk/go/httpserver"
25         "github.com/prometheus/client_golang/prometheus"
26         "github.com/sirupsen/logrus"
27         check "gopkg.in/check.v1"
28 )
29
30 var _ = check.Suite(&serviceSuite{})
31
32 type serviceSuite struct {
33         handler service.Handler
34         reg     *prometheus.Registry
35         srv     *httptest.Server
36         cluster *arvados.Cluster
37         wg      sync.WaitGroup
38 }
39
40 func (s *serviceSuite) SetUpTest(c *check.C) {
41         var err error
42         s.cluster, err = s.testConfig(c)
43         c.Assert(err, check.IsNil)
44 }
45
46 func (s *serviceSuite) start(c *check.C) {
47         s.reg = prometheus.NewRegistry()
48         s.handler = newHandler(context.Background(), s.cluster, "", s.reg)
49         instrumented := httpserver.Instrument(s.reg, ctxlog.TestLogger(c), s.handler)
50         s.srv = httptest.NewServer(instrumented.ServeAPI(s.cluster.ManagementToken, instrumented))
51 }
52
53 func (s *serviceSuite) TearDownTest(c *check.C) {
54         if s.srv != nil {
55                 s.srv.Close()
56         }
57 }
58
59 func (*serviceSuite) testConfig(c *check.C) (*arvados.Cluster, error) {
60         ldr := config.NewLoader(nil, ctxlog.TestLogger(c))
61         cfg, err := ldr.Load()
62         if err != nil {
63                 return nil, err
64         }
65         cluster, err := cfg.GetCluster("")
66         if err != nil {
67                 return nil, err
68         }
69         client := arvados.NewClientFromEnv()
70         cluster.Services.Controller.ExternalURL.Host = client.APIHost
71         cluster.SystemRootToken = client.AuthToken
72         cluster.TLS.Insecure = client.Insecure
73         cluster.PostgreSQL.Connection = testDBConfig()
74         cluster.PostgreSQL.ConnectionPool = 12
75         cluster.Services.Websocket.InternalURLs = map[arvados.URL]arvados.ServiceInstance{{Host: ":"}: {}}
76         cluster.ManagementToken = arvadostest.ManagementToken
77         return cluster, nil
78 }
79
80 // TestBadDB ensures the server returns an error (instead of panicking
81 // or deadlocking) if it can't connect to the database server at
82 // startup.
83 func (s *serviceSuite) TestBadDB(c *check.C) {
84         s.cluster.PostgreSQL.Connection["password"] = "1234"
85         s.start(c)
86         resp, err := http.Get(s.srv.URL)
87         c.Check(err, check.IsNil)
88         c.Check(resp.StatusCode, check.Equals, http.StatusInternalServerError)
89         c.Check(s.handler.CheckHealth(), check.ErrorMatches, "database not connected")
90         c.Check(err, check.IsNil)
91         c.Check(resp.StatusCode, check.Equals, http.StatusInternalServerError)
92 }
93
94 func (s *serviceSuite) TestHealth(c *check.C) {
95         s.start(c)
96         for _, token := range []string{"", "foo", s.cluster.ManagementToken} {
97                 req, err := http.NewRequest("GET", s.srv.URL+"/_health/ping", nil)
98                 c.Assert(err, check.IsNil)
99                 if token != "" {
100                         req.Header.Add("Authorization", "Bearer "+token)
101                 }
102                 resp, err := http.DefaultClient.Do(req)
103                 c.Check(err, check.IsNil)
104                 if token == s.cluster.ManagementToken {
105                         c.Check(resp.StatusCode, check.Equals, http.StatusOK)
106                         buf, err := ioutil.ReadAll(resp.Body)
107                         c.Check(err, check.IsNil)
108                         c.Check(string(buf), check.Equals, `{"health":"OK"}`+"\n")
109                 } else {
110                         c.Check(resp.StatusCode, check.Not(check.Equals), http.StatusOK)
111                 }
112         }
113 }
114
115 func (s *serviceSuite) TestMetrics(c *check.C) {
116         s.start(c)
117         s.handler.CheckHealth()
118         for deadline := time.Now().Add(time.Second); ; {
119                 req, err := http.NewRequest("GET", s.srv.URL+"/metrics", nil)
120                 c.Assert(err, check.IsNil)
121                 req.Header.Set("Authorization", "Bearer "+s.cluster.ManagementToken)
122                 resp, err := http.DefaultClient.Do(req)
123                 c.Check(err, check.IsNil)
124                 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
125                 text, err := ioutil.ReadAll(resp.Body)
126                 c.Check(err, check.IsNil)
127                 if strings.Contains(string(text), "_db_max_connections 0\n") {
128                         // wait for the first db stats update
129                         if time.Now().After(deadline) {
130                                 c.Fatal("timed out")
131                         }
132                         time.Sleep(time.Second / 50)
133                         continue
134                 }
135                 c.Check(string(text), check.Matches, `(?ms).*\narvados_ws_db_max_connections 12\n.*`)
136                 c.Check(string(text), check.Matches, `(?ms).*\narvados_ws_db_open_connections\{inuse="0"\} \d+\n.*`)
137                 c.Check(string(text), check.Matches, `(?ms).*\narvados_ws_db_open_connections\{inuse="1"\} \d+\n.*`)
138                 break
139         }
140 }
141
142 func (s *serviceSuite) TestHealthDisabled(c *check.C) {
143         s.cluster.ManagementToken = ""
144         s.start(c)
145         for _, token := range []string{"", "foo", arvadostest.ManagementToken} {
146                 req, err := http.NewRequest("GET", s.srv.URL+"/_health/ping", nil)
147                 c.Assert(err, check.IsNil)
148                 req.Header.Add("Authorization", "Bearer "+token)
149                 resp, err := http.DefaultClient.Do(req)
150                 c.Check(err, check.IsNil)
151                 c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
152         }
153 }
154
155 func (s *serviceSuite) TestLoadLegacyConfig(c *check.C) {
156         content := []byte(`
157 Client:
158   APIHost: example.com
159   AuthToken: abcdefg
160 Postgres:
161   "dbname": "arvados_production"
162   "user": "arvados"
163   "password": "xyzzy"
164   "host": "localhost"
165   "connect_timeout": "30"
166   "sslmode": "require"
167   "fallback_application_name": "arvados-ws"
168 PostgresPool: 63
169 Listen: ":8765"
170 LogLevel: "debug"
171 LogFormat: "text"
172 PingTimeout: 61s
173 ClientEventQueue: 62
174 ServerEventQueue:  5
175 ManagementToken: qqqqq
176 `)
177         tmpfile, err := ioutil.TempFile("", "example")
178         if err != nil {
179                 c.Error(err)
180         }
181
182         defer os.Remove(tmpfile.Name()) // clean up
183
184         if _, err := tmpfile.Write(content); err != nil {
185                 c.Error(err)
186         }
187         if err := tmpfile.Close(); err != nil {
188                 c.Error(err)
189
190         }
191         ldr := config.NewLoader(&bytes.Buffer{}, logrus.New())
192         flagset := flag.NewFlagSet("", flag.ContinueOnError)
193         ldr.SetupFlags(flagset)
194         flagset.Parse(ldr.MungeLegacyConfigArgs(ctxlog.TestLogger(c), []string{"-config", tmpfile.Name()}, "-legacy-ws-config"))
195         cfg, err := ldr.Load()
196         c.Check(err, check.IsNil)
197         cluster, err := cfg.GetCluster("")
198         c.Check(err, check.IsNil)
199         c.Check(cluster, check.NotNil)
200
201         c.Check(cluster.Services.Controller.ExternalURL, check.Equals, arvados.URL{Scheme: "https", Host: "example.com", Path: "/"})
202         c.Check(cluster.SystemRootToken, check.Equals, "abcdefg")
203
204         c.Check(cluster.PostgreSQL.Connection, check.DeepEquals, arvados.PostgreSQLConnection{
205                 "connect_timeout":           "30",
206                 "dbname":                    "arvados_production",
207                 "fallback_application_name": "arvados-ws",
208                 "host":                      "localhost",
209                 "password":                  "xyzzy",
210                 "sslmode":                   "require",
211                 "user":                      "arvados"})
212         c.Check(cluster.PostgreSQL.ConnectionPool, check.Equals, 63)
213         c.Check(cluster.Services.Websocket.InternalURLs[arvados.URL{Host: ":8765"}], check.NotNil)
214         c.Check(cluster.SystemLogs.LogLevel, check.Equals, "debug")
215         c.Check(cluster.SystemLogs.Format, check.Equals, "text")
216         c.Check(cluster.API.SendTimeout, check.Equals, arvados.Duration(61*time.Second))
217         c.Check(cluster.API.WebsocketClientEventQueue, check.Equals, 62)
218         c.Check(cluster.API.WebsocketServerEventQueue, check.Equals, 5)
219         c.Check(cluster.ManagementToken, check.Equals, "qqqqq")
220 }