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