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