17800: Fixes bug.
[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) TestCreateLarge(c *C) {
162         arv, err := MakeArvadosClient()
163         c.Assert(err, IsNil)
164
165         txt := arvados.SignLocator("d41d8cd98f00b204e9800998ecf8427e+0", arv.ApiToken, time.Now().Add(time.Minute), time.Minute, []byte(arvadostest.SystemRootToken))
166         // Ensure our request body is bigger than the Go http server's
167         // default max size, 10 MB.
168         for len(txt) < 12000000 {
169                 txt = txt + " " + txt
170         }
171         txt = ". " + txt + " 0:0:foo\n"
172
173         resp := Dict{}
174         err = arv.Create("collections", Dict{
175                 "ensure_unique_name": true,
176                 "collection": Dict{
177                         "is_trashed":    true,
178                         "name":          "test",
179                         "manifest_text": txt,
180                 },
181         }, &resp)
182         c.Check(err, IsNil)
183         c.Check(resp["portable_data_hash"], Not(Equals), "")
184         c.Check(resp["portable_data_hash"], Not(Equals), "d41d8cd98f00b204e9800998ecf8427e+0")
185 }
186
187 type UnitSuite struct{}
188
189 func (s *UnitSuite) TestUUIDMatch(c *C) {
190         c.Assert(UUIDMatch("zzzzz-tpzed-000000000000000"), Equals, true)
191         c.Assert(UUIDMatch("zzzzz-zebra-000000000000000"), Equals, true)
192         c.Assert(UUIDMatch("00000-00000-zzzzzzzzzzzzzzz"), Equals, true)
193         c.Assert(UUIDMatch("ZEBRA-HORSE-AFRICANELEPHANT"), Equals, false)
194         c.Assert(UUIDMatch(" zzzzz-tpzed-000000000000000"), Equals, false)
195         c.Assert(UUIDMatch("d41d8cd98f00b204e9800998ecf8427e"), Equals, false)
196         c.Assert(UUIDMatch("d41d8cd98f00b204e9800998ecf8427e+0"), Equals, false)
197         c.Assert(UUIDMatch(""), Equals, false)
198 }
199
200 func (s *UnitSuite) TestPDHMatch(c *C) {
201         c.Assert(PDHMatch("zzzzz-tpzed-000000000000000"), Equals, false)
202         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e"), Equals, false)
203         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+0"), Equals, true)
204         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345"), Equals, true)
205         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e 12345"), Equals, false)
206         c.Assert(PDHMatch("D41D8CD98F00B204E9800998ECF8427E+12345"), Equals, false)
207         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345 "), Equals, false)
208         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+abcdef"), Equals, false)
209         c.Assert(PDHMatch("da39a3ee5e6b4b0d3255bfef95601890afd80709"), Equals, false)
210         c.Assert(PDHMatch("da39a3ee5e6b4b0d3255bfef95601890afd80709+0"), Equals, false)
211         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427+12345"), Equals, false)
212         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345\n"), Equals, false)
213         c.Assert(PDHMatch("+12345"), Equals, false)
214         c.Assert(PDHMatch(""), Equals, false)
215 }
216
217 // Tests that use mock arvados server
218 type MockArvadosServerSuite struct{}
219
220 func (s *MockArvadosServerSuite) SetUpSuite(c *C) {
221         RetryDelay = 0
222 }
223
224 func (s *MockArvadosServerSuite) SetUpTest(c *C) {
225         arvadostest.ResetEnv()
226 }
227
228 type APIServer struct {
229         listener net.Listener
230         url      string
231 }
232
233 func RunFakeArvadosServer(st http.Handler) (api APIServer, err error) {
234         api.listener, err = net.ListenTCP("tcp", &net.TCPAddr{Port: 0})
235         if err != nil {
236                 return
237         }
238         api.url = api.listener.Addr().String()
239         go http.Serve(api.listener, st)
240         return
241 }
242
243 type APIStub struct {
244         method        string
245         retryAttempts int
246         expected      int
247         respStatus    []int
248         responseBody  []string
249 }
250
251 func (h *APIStub) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
252         if req.URL.Path == "/redirect-loop" {
253                 http.Redirect(resp, req, "/redirect-loop", http.StatusFound)
254                 return
255         }
256         if h.respStatus[h.retryAttempts] < 0 {
257                 // Fail the client's Do() by starting a redirect loop
258                 http.Redirect(resp, req, "/redirect-loop", http.StatusFound)
259         } else {
260                 resp.WriteHeader(h.respStatus[h.retryAttempts])
261                 resp.Write([]byte(h.responseBody[h.retryAttempts]))
262         }
263         h.retryAttempts++
264 }
265
266 func (s *MockArvadosServerSuite) TestWithRetries(c *C) {
267         for _, stub := range []APIStub{
268                 {
269                         "get", 0, 200, []int{200, 500}, []string{`{"ok":"ok"}`, ``},
270                 },
271                 {
272                         "create", 0, 200, []int{200, 500}, []string{`{"ok":"ok"}`, ``},
273                 },
274                 {
275                         "get", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
276                 },
277                 {
278                         "create", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
279                 },
280                 {
281                         "update", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
282                 },
283                 {
284                         "delete", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
285                 },
286                 {
287                         "get", 0, 502, []int{500, 500, 502, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
288                 },
289                 {
290                         "create", 0, 502, []int{500, 500, 502, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
291                 },
292                 {
293                         "get", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
294                 },
295                 {
296                         "create", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
297                 },
298                 {
299                         "delete", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
300                 },
301                 {
302                         "update", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
303                 },
304                 {
305                         "get", 0, 401, []int{401, 200}, []string{``, `{"ok":"ok"}`},
306                 },
307                 {
308                         "create", 0, 401, []int{401, 200}, []string{``, `{"ok":"ok"}`},
309                 },
310                 {
311                         "get", 0, 404, []int{404, 200}, []string{``, `{"ok":"ok"}`},
312                 },
313                 {
314                         "get", 0, 401, []int{500, 401, 200}, []string{``, ``, `{"ok":"ok"}`},
315                 },
316
317                 // Response code -1 simulates an HTTP/network error
318                 // (i.e., Do() returns an error; there is no HTTP
319                 // response status code).
320
321                 // Succeed on second retry
322                 {
323                         "get", 0, 200, []int{-1, -1, 200}, []string{``, ``, `{"ok":"ok"}`},
324                 },
325                 // "POST" is not safe to retry: fail after one error
326                 {
327                         "create", 0, -1, []int{-1, 200}, []string{``, `{"ok":"ok"}`},
328                 },
329         } {
330                 api, err := RunFakeArvadosServer(&stub)
331                 c.Check(err, IsNil)
332
333                 defer api.listener.Close()
334
335                 arv := ArvadosClient{
336                         Scheme:      "http",
337                         ApiServer:   api.url,
338                         ApiToken:    "abc123",
339                         ApiInsecure: true,
340                         Client:      &http.Client{Transport: &http.Transport{}},
341                         Retries:     2}
342
343                 getback := make(Dict)
344                 switch stub.method {
345                 case "get":
346                         err = arv.Get("collections", "zzzzz-4zz18-znfnqtbbv4spc3w", nil, &getback)
347                 case "create":
348                         err = arv.Create("collections",
349                                 Dict{"collection": Dict{"name": "testing"}},
350                                 &getback)
351                 case "update":
352                         err = arv.Update("collections", "zzzzz-4zz18-znfnqtbbv4spc3w",
353                                 Dict{"collection": Dict{"name": "testing"}},
354                                 &getback)
355                 case "delete":
356                         err = arv.Delete("pipeline_templates", "zzzzz-4zz18-znfnqtbbv4spc3w", nil, &getback)
357                 }
358
359                 switch stub.expected {
360                 case 200:
361                         c.Check(err, IsNil)
362                         c.Check(getback["ok"], Equals, "ok")
363                 case -1:
364                         c.Check(err, NotNil)
365                         c.Check(err, ErrorMatches, `.*stopped after \d+ redirects`)
366                 default:
367                         c.Check(err, NotNil)
368                         c.Check(err, ErrorMatches, fmt.Sprintf("arvados API server error: %d.*", stub.expected))
369                         c.Check(err.(APIServerError).HttpStatusCode, Equals, stub.expected)
370                 }
371         }
372 }