18995: Merge branch 'main' into 18995-code-cleanup-5
[arvados.git] / sdk / go / arvadosclient / arvadosclient_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvadosclient
6
7 import (
8         "fmt"
9         "net"
10         "net/http"
11         "os"
12         "testing"
13         "time"
14
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         "git.arvados.org/arvados.git/sdk/go/arvadostest"
17         . "gopkg.in/check.v1"
18 )
19
20 // Gocheck boilerplate
21 func Test(t *testing.T) {
22         TestingT(t)
23 }
24
25 var _ = Suite(&ServerRequiredSuite{})
26 var _ = Suite(&UnitSuite{})
27 var _ = Suite(&MockArvadosServerSuite{})
28
29 // Tests that require the Keep server running
30 type ServerRequiredSuite struct{}
31
32 func (s *ServerRequiredSuite) SetUpSuite(c *C) {
33         arvadostest.StartKeep(2, false)
34         RetryDelay = 0
35 }
36
37 func (s *ServerRequiredSuite) TearDownSuite(c *C) {
38         arvadostest.StopKeep(2)
39 }
40
41 func (s *ServerRequiredSuite) SetUpTest(c *C) {
42         arvadostest.ResetEnv()
43 }
44
45 func (s *ServerRequiredSuite) TestMakeArvadosClientSecure(c *C) {
46         os.Setenv("ARVADOS_API_HOST_INSECURE", "")
47         ac, err := MakeArvadosClient()
48         c.Assert(err, Equals, nil)
49         c.Check(ac.ApiServer, Equals, os.Getenv("ARVADOS_API_HOST"))
50         c.Check(ac.ApiToken, Equals, os.Getenv("ARVADOS_API_TOKEN"))
51         c.Check(ac.ApiInsecure, Equals, false)
52 }
53
54 func (s *ServerRequiredSuite) TestMakeArvadosClientInsecure(c *C) {
55         os.Setenv("ARVADOS_API_HOST_INSECURE", "true")
56         ac, err := MakeArvadosClient()
57         c.Assert(err, Equals, nil)
58         c.Check(ac.ApiInsecure, Equals, true)
59         c.Check(ac.ApiServer, Equals, os.Getenv("ARVADOS_API_HOST"))
60         c.Check(ac.ApiToken, Equals, os.Getenv("ARVADOS_API_TOKEN"))
61         c.Check(ac.Client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, Equals, true)
62 }
63
64 func (s *ServerRequiredSuite) TestGetInvalidUUID(c *C) {
65         arv, err := MakeArvadosClient()
66         c.Assert(err, IsNil)
67
68         getback := make(Dict)
69         err = arv.Get("collections", "", nil, &getback)
70         c.Assert(err, Equals, ErrInvalidArgument)
71         c.Assert(len(getback), Equals, 0)
72
73         err = arv.Get("collections", "zebra-moose-unicorn", nil, &getback)
74         c.Assert(err, Equals, ErrInvalidArgument)
75         c.Assert(len(getback), Equals, 0)
76
77         err = arv.Get("collections", "acbd18db4cc2f85cedef654fccc4a4d8", nil, &getback)
78         c.Assert(err, Equals, ErrInvalidArgument)
79         c.Assert(len(getback), Equals, 0)
80 }
81
82 func (s *ServerRequiredSuite) TestGetValidUUID(c *C) {
83         arv, err := MakeArvadosClient()
84         c.Assert(err, IsNil)
85
86         getback := make(Dict)
87         err = arv.Get("collections", "zzzzz-4zz18-abcdeabcdeabcde", nil, &getback)
88         c.Assert(err, FitsTypeOf, APIServerError{})
89         c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
90         c.Assert(len(getback), Equals, 0)
91
92         err = arv.Get("collections", "acbd18db4cc2f85cedef654fccc4a4d8+3", nil, &getback)
93         c.Assert(err, FitsTypeOf, APIServerError{})
94         c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
95         c.Assert(len(getback), Equals, 0)
96 }
97
98 func (s *ServerRequiredSuite) TestInvalidResourceType(c *C) {
99         arv, err := MakeArvadosClient()
100         c.Assert(err, IsNil)
101
102         getback := make(Dict)
103         err = arv.Get("unicorns", "zzzzz-zebra-unicorn7unicorn", nil, &getback)
104         c.Assert(err, FitsTypeOf, APIServerError{})
105         c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
106         c.Assert(len(getback), Equals, 0)
107
108         err = arv.Update("unicorns", "zzzzz-zebra-unicorn7unicorn", nil, &getback)
109         c.Assert(err, FitsTypeOf, APIServerError{})
110         c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
111         c.Assert(len(getback), Equals, 0)
112
113         err = arv.List("unicorns", nil, &getback)
114         c.Assert(err, FitsTypeOf, APIServerError{})
115         c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
116         c.Assert(len(getback), Equals, 0)
117 }
118
119 func (s *ServerRequiredSuite) TestErrorResponse(c *C) {
120         arv, _ := MakeArvadosClient()
121
122         getback := make(Dict)
123
124         {
125                 err := arv.Create("logs",
126                         Dict{"log": Dict{"bogus_attr": "foo"}},
127                         &getback)
128                 c.Assert(err, ErrorMatches, "arvados API server error: .*")
129                 c.Assert(err, ErrorMatches, ".*unknown attribute(: | ')bogus_attr.*")
130                 c.Assert(err, FitsTypeOf, APIServerError{})
131                 c.Assert(err.(APIServerError).HttpStatusCode, Equals, 422)
132         }
133
134         {
135                 err := arv.Create("bogus",
136                         Dict{"bogus": Dict{}},
137                         &getback)
138                 c.Assert(err, ErrorMatches, "arvados API server error: .*")
139                 c.Assert(err, ErrorMatches, ".*Path not found.*")
140                 c.Assert(err, FitsTypeOf, APIServerError{})
141                 c.Assert(err.(APIServerError).HttpStatusCode, Equals, 404)
142         }
143 }
144
145 func (s *ServerRequiredSuite) TestAPIDiscovery_Get_defaultCollectionReplication(c *C) {
146         arv, err := MakeArvadosClient()
147         c.Assert(err, IsNil)
148         value, err := arv.Discovery("defaultCollectionReplication")
149         c.Assert(err, IsNil)
150         c.Assert(value, NotNil)
151 }
152
153 func (s *ServerRequiredSuite) TestAPIDiscovery_Get_noSuchParameter(c *C) {
154         arv, err := MakeArvadosClient()
155         c.Assert(err, IsNil)
156         value, err := arv.Discovery("noSuchParameter")
157         c.Assert(err, NotNil)
158         c.Assert(value, IsNil)
159 }
160
161 func (s *ServerRequiredSuite) TestAPIClusterConfig_Get_StorageClasses(c *C) {
162         arv, err := MakeArvadosClient()
163         c.Assert(err, IsNil)
164         data, err := arv.ClusterConfig("StorageClasses")
165         c.Assert(err, IsNil)
166         c.Assert(data, NotNil)
167         clusterConfig := data.(map[string]interface{})
168         _, ok := clusterConfig["default"]
169         c.Assert(ok, Equals, true)
170 }
171
172 func (s *ServerRequiredSuite) TestAPIClusterConfig_Get_All(c *C) {
173         arv, err := MakeArvadosClient()
174         c.Assert(err, IsNil)
175         data, err := arv.ClusterConfig("")
176         c.Assert(err, IsNil)
177         c.Assert(data, NotNil)
178         clusterConfig := data.(map[string]interface{})
179         _, ok := clusterConfig["StorageClasses"]
180         c.Assert(ok, Equals, true)
181 }
182
183 func (s *ServerRequiredSuite) TestAPIClusterConfig_Get_noSuchSection(c *C) {
184         arv, err := MakeArvadosClient()
185         c.Assert(err, IsNil)
186         data, err := arv.ClusterConfig("noSuchSection")
187         c.Assert(err, NotNil)
188         c.Assert(data, IsNil)
189 }
190
191 func (s *ServerRequiredSuite) TestCreateLarge(c *C) {
192         arv, err := MakeArvadosClient()
193         c.Assert(err, IsNil)
194
195         txt := arvados.SignLocator("d41d8cd98f00b204e9800998ecf8427e+0", arv.ApiToken, time.Now().Add(time.Minute), time.Minute, []byte(arvadostest.SystemRootToken))
196         // Ensure our request body is bigger than the Go http server's
197         // default max size, 10 MB.
198         for len(txt) < 12000000 {
199                 txt = txt + " " + txt
200         }
201         txt = ". " + txt + " 0:0:foo\n"
202
203         resp := Dict{}
204         err = arv.Create("collections", Dict{
205                 "ensure_unique_name": true,
206                 "collection": Dict{
207                         "is_trashed":    true,
208                         "name":          "test",
209                         "manifest_text": txt,
210                 },
211         }, &resp)
212         c.Check(err, IsNil)
213         c.Check(resp["portable_data_hash"], Not(Equals), "")
214         c.Check(resp["portable_data_hash"], Not(Equals), "d41d8cd98f00b204e9800998ecf8427e+0")
215 }
216
217 type UnitSuite struct{}
218
219 func (s *UnitSuite) TestUUIDMatch(c *C) {
220         c.Assert(UUIDMatch("zzzzz-tpzed-000000000000000"), Equals, true)
221         c.Assert(UUIDMatch("zzzzz-zebra-000000000000000"), Equals, true)
222         c.Assert(UUIDMatch("00000-00000-zzzzzzzzzzzzzzz"), Equals, true)
223         c.Assert(UUIDMatch("ZEBRA-HORSE-AFRICANELEPHANT"), Equals, false)
224         c.Assert(UUIDMatch(" zzzzz-tpzed-000000000000000"), Equals, false)
225         c.Assert(UUIDMatch("d41d8cd98f00b204e9800998ecf8427e"), Equals, false)
226         c.Assert(UUIDMatch("d41d8cd98f00b204e9800998ecf8427e+0"), Equals, false)
227         c.Assert(UUIDMatch(""), Equals, false)
228 }
229
230 func (s *UnitSuite) TestPDHMatch(c *C) {
231         c.Assert(PDHMatch("zzzzz-tpzed-000000000000000"), Equals, false)
232         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e"), Equals, false)
233         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+0"), Equals, true)
234         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345"), Equals, true)
235         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e 12345"), Equals, false)
236         c.Assert(PDHMatch("D41D8CD98F00B204E9800998ECF8427E+12345"), Equals, false)
237         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345 "), Equals, false)
238         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+abcdef"), Equals, false)
239         c.Assert(PDHMatch("da39a3ee5e6b4b0d3255bfef95601890afd80709"), Equals, false)
240         c.Assert(PDHMatch("da39a3ee5e6b4b0d3255bfef95601890afd80709+0"), Equals, false)
241         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427+12345"), Equals, false)
242         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345\n"), Equals, false)
243         c.Assert(PDHMatch("+12345"), Equals, false)
244         c.Assert(PDHMatch(""), Equals, false)
245 }
246
247 // Tests that use mock arvados server
248 type MockArvadosServerSuite struct{}
249
250 func (s *MockArvadosServerSuite) SetUpSuite(c *C) {
251         RetryDelay = 0
252 }
253
254 func (s *MockArvadosServerSuite) SetUpTest(c *C) {
255         arvadostest.ResetEnv()
256 }
257
258 type APIServer struct {
259         listener net.Listener
260         url      string
261 }
262
263 func RunFakeArvadosServer(st http.Handler) (api APIServer, err error) {
264         api.listener, err = net.ListenTCP("tcp", &net.TCPAddr{Port: 0})
265         if err != nil {
266                 return
267         }
268         api.url = api.listener.Addr().String()
269         go http.Serve(api.listener, st)
270         return
271 }
272
273 type APIStub struct {
274         method        string
275         retryAttempts int
276         expected      int
277         respStatus    []int
278         responseBody  []string
279 }
280
281 func (h *APIStub) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
282         if req.URL.Path == "/redirect-loop" {
283                 http.Redirect(resp, req, "/redirect-loop", http.StatusFound)
284                 return
285         }
286         if h.respStatus[h.retryAttempts] < 0 {
287                 // Fail the client's Do() by starting a redirect loop
288                 http.Redirect(resp, req, "/redirect-loop", http.StatusFound)
289         } else {
290                 resp.WriteHeader(h.respStatus[h.retryAttempts])
291                 resp.Write([]byte(h.responseBody[h.retryAttempts]))
292         }
293         h.retryAttempts++
294 }
295
296 func (s *MockArvadosServerSuite) TestWithRetries(c *C) {
297         for _, stub := range []APIStub{
298                 {
299                         "get", 0, 200, []int{200, 500}, []string{`{"ok":"ok"}`, ``},
300                 },
301                 {
302                         "create", 0, 200, []int{200, 500}, []string{`{"ok":"ok"}`, ``},
303                 },
304                 {
305                         "get", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
306                 },
307                 {
308                         "create", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
309                 },
310                 {
311                         "update", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
312                 },
313                 {
314                         "delete", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
315                 },
316                 {
317                         "get", 0, 502, []int{500, 500, 502, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
318                 },
319                 {
320                         "create", 0, 502, []int{500, 500, 502, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
321                 },
322                 {
323                         "get", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
324                 },
325                 {
326                         "create", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
327                 },
328                 {
329                         "delete", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
330                 },
331                 {
332                         "update", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
333                 },
334                 {
335                         "get", 0, 401, []int{401, 200}, []string{``, `{"ok":"ok"}`},
336                 },
337                 {
338                         "create", 0, 401, []int{401, 200}, []string{``, `{"ok":"ok"}`},
339                 },
340                 {
341                         "get", 0, 404, []int{404, 200}, []string{``, `{"ok":"ok"}`},
342                 },
343                 {
344                         "get", 0, 401, []int{500, 401, 200}, []string{``, ``, `{"ok":"ok"}`},
345                 },
346
347                 // Response code -1 simulates an HTTP/network error
348                 // (i.e., Do() returns an error; there is no HTTP
349                 // response status code).
350
351                 // Succeed on second retry
352                 {
353                         "get", 0, 200, []int{-1, -1, 200}, []string{``, ``, `{"ok":"ok"}`},
354                 },
355                 // "POST" is not safe to retry: fail after one error
356                 {
357                         "create", 0, -1, []int{-1, 200}, []string{``, `{"ok":"ok"}`},
358                 },
359         } {
360                 api, err := RunFakeArvadosServer(&stub)
361                 c.Check(err, IsNil)
362
363                 defer api.listener.Close()
364
365                 arv := ArvadosClient{
366                         Scheme:      "http",
367                         ApiServer:   api.url,
368                         ApiToken:    "abc123",
369                         ApiInsecure: true,
370                         Client:      &http.Client{Transport: &http.Transport{}},
371                         Retries:     2}
372
373                 getback := make(Dict)
374                 switch stub.method {
375                 case "get":
376                         err = arv.Get("collections", "zzzzz-4zz18-znfnqtbbv4spc3w", nil, &getback)
377                 case "create":
378                         err = arv.Create("collections",
379                                 Dict{"collection": Dict{"name": "testing"}},
380                                 &getback)
381                 case "update":
382                         err = arv.Update("collections", "zzzzz-4zz18-znfnqtbbv4spc3w",
383                                 Dict{"collection": Dict{"name": "testing"}},
384                                 &getback)
385                 case "delete":
386                         err = arv.Delete("pipeline_templates", "zzzzz-4zz18-znfnqtbbv4spc3w", nil, &getback)
387                 }
388
389                 switch stub.expected {
390                 case 200:
391                         c.Check(err, IsNil)
392                         c.Check(getback["ok"], Equals, "ok")
393                 case -1:
394                         c.Check(err, NotNil)
395                         c.Check(err, ErrorMatches, `.*stopped after \d+ redirects`)
396                 default:
397                         c.Check(err, NotNil)
398                         c.Check(err, ErrorMatches, fmt.Sprintf("arvados API server error: %d.*", stub.expected))
399                         c.Check(err.(APIServerError).HttpStatusCode, Equals, stub.expected)
400                 }
401         }
402 }