17204: Fix misleading log message on 304 Not Modified response.
[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
14         "git.arvados.org/arvados.git/sdk/go/arvadostest"
15         . "gopkg.in/check.v1"
16 )
17
18 // Gocheck boilerplate
19 func Test(t *testing.T) {
20         TestingT(t)
21 }
22
23 var _ = Suite(&ServerRequiredSuite{})
24 var _ = Suite(&UnitSuite{})
25 var _ = Suite(&MockArvadosServerSuite{})
26
27 // Tests that require the Keep server running
28 type ServerRequiredSuite struct{}
29
30 func (s *ServerRequiredSuite) SetUpSuite(c *C) {
31         arvadostest.StartAPI()
32         arvadostest.StartKeep(2, false)
33         RetryDelay = 0
34 }
35
36 func (s *ServerRequiredSuite) TearDownSuite(c *C) {
37         arvadostest.StopKeep(2)
38         arvadostest.StopAPI()
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 type UnitSuite struct{}
162
163 func (s *UnitSuite) TestUUIDMatch(c *C) {
164         c.Assert(UUIDMatch("zzzzz-tpzed-000000000000000"), Equals, true)
165         c.Assert(UUIDMatch("zzzzz-zebra-000000000000000"), Equals, true)
166         c.Assert(UUIDMatch("00000-00000-zzzzzzzzzzzzzzz"), Equals, true)
167         c.Assert(UUIDMatch("ZEBRA-HORSE-AFRICANELEPHANT"), Equals, false)
168         c.Assert(UUIDMatch(" zzzzz-tpzed-000000000000000"), Equals, false)
169         c.Assert(UUIDMatch("d41d8cd98f00b204e9800998ecf8427e"), Equals, false)
170         c.Assert(UUIDMatch("d41d8cd98f00b204e9800998ecf8427e+0"), Equals, false)
171         c.Assert(UUIDMatch(""), Equals, false)
172 }
173
174 func (s *UnitSuite) TestPDHMatch(c *C) {
175         c.Assert(PDHMatch("zzzzz-tpzed-000000000000000"), Equals, false)
176         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e"), Equals, false)
177         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+0"), Equals, true)
178         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345"), Equals, true)
179         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e 12345"), Equals, false)
180         c.Assert(PDHMatch("D41D8CD98F00B204E9800998ECF8427E+12345"), Equals, false)
181         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345 "), Equals, false)
182         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+abcdef"), Equals, false)
183         c.Assert(PDHMatch("da39a3ee5e6b4b0d3255bfef95601890afd80709"), Equals, false)
184         c.Assert(PDHMatch("da39a3ee5e6b4b0d3255bfef95601890afd80709+0"), Equals, false)
185         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427+12345"), Equals, false)
186         c.Assert(PDHMatch("d41d8cd98f00b204e9800998ecf8427e+12345\n"), Equals, false)
187         c.Assert(PDHMatch("+12345"), Equals, false)
188         c.Assert(PDHMatch(""), Equals, false)
189 }
190
191 // Tests that use mock arvados server
192 type MockArvadosServerSuite struct{}
193
194 func (s *MockArvadosServerSuite) SetUpSuite(c *C) {
195         RetryDelay = 0
196 }
197
198 func (s *MockArvadosServerSuite) SetUpTest(c *C) {
199         arvadostest.ResetEnv()
200 }
201
202 type APIServer struct {
203         listener net.Listener
204         url      string
205 }
206
207 func RunFakeArvadosServer(st http.Handler) (api APIServer, err error) {
208         api.listener, err = net.ListenTCP("tcp", &net.TCPAddr{Port: 0})
209         if err != nil {
210                 return
211         }
212         api.url = api.listener.Addr().String()
213         go http.Serve(api.listener, st)
214         return
215 }
216
217 type APIStub struct {
218         method        string
219         retryAttempts int
220         expected      int
221         respStatus    []int
222         responseBody  []string
223 }
224
225 func (h *APIStub) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
226         if req.URL.Path == "/redirect-loop" {
227                 http.Redirect(resp, req, "/redirect-loop", http.StatusFound)
228                 return
229         }
230         if h.respStatus[h.retryAttempts] < 0 {
231                 // Fail the client's Do() by starting a redirect loop
232                 http.Redirect(resp, req, "/redirect-loop", http.StatusFound)
233         } else {
234                 resp.WriteHeader(h.respStatus[h.retryAttempts])
235                 resp.Write([]byte(h.responseBody[h.retryAttempts]))
236         }
237         h.retryAttempts++
238 }
239
240 func (s *MockArvadosServerSuite) TestWithRetries(c *C) {
241         for _, stub := range []APIStub{
242                 {
243                         "get", 0, 200, []int{200, 500}, []string{`{"ok":"ok"}`, ``},
244                 },
245                 {
246                         "create", 0, 200, []int{200, 500}, []string{`{"ok":"ok"}`, ``},
247                 },
248                 {
249                         "get", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
250                 },
251                 {
252                         "create", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
253                 },
254                 {
255                         "update", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
256                 },
257                 {
258                         "delete", 0, 500, []int{500, 500, 500, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
259                 },
260                 {
261                         "get", 0, 502, []int{500, 500, 502, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
262                 },
263                 {
264                         "create", 0, 502, []int{500, 500, 502, 200}, []string{``, ``, ``, `{"ok":"ok"}`},
265                 },
266                 {
267                         "get", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
268                 },
269                 {
270                         "create", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
271                 },
272                 {
273                         "delete", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
274                 },
275                 {
276                         "update", 0, 200, []int{500, 500, 200}, []string{``, ``, `{"ok":"ok"}`},
277                 },
278                 {
279                         "get", 0, 401, []int{401, 200}, []string{``, `{"ok":"ok"}`},
280                 },
281                 {
282                         "create", 0, 401, []int{401, 200}, []string{``, `{"ok":"ok"}`},
283                 },
284                 {
285                         "get", 0, 404, []int{404, 200}, []string{``, `{"ok":"ok"}`},
286                 },
287                 {
288                         "get", 0, 401, []int{500, 401, 200}, []string{``, ``, `{"ok":"ok"}`},
289                 },
290
291                 // Response code -1 simulates an HTTP/network error
292                 // (i.e., Do() returns an error; there is no HTTP
293                 // response status code).
294
295                 // Succeed on second retry
296                 {
297                         "get", 0, 200, []int{-1, -1, 200}, []string{``, ``, `{"ok":"ok"}`},
298                 },
299                 // "POST" is not safe to retry: fail after one error
300                 {
301                         "create", 0, -1, []int{-1, 200}, []string{``, `{"ok":"ok"}`},
302                 },
303         } {
304                 api, err := RunFakeArvadosServer(&stub)
305                 c.Check(err, IsNil)
306
307                 defer api.listener.Close()
308
309                 arv := ArvadosClient{
310                         Scheme:      "http",
311                         ApiServer:   api.url,
312                         ApiToken:    "abc123",
313                         ApiInsecure: true,
314                         Client:      &http.Client{Transport: &http.Transport{}},
315                         Retries:     2}
316
317                 getback := make(Dict)
318                 switch stub.method {
319                 case "get":
320                         err = arv.Get("collections", "zzzzz-4zz18-znfnqtbbv4spc3w", nil, &getback)
321                 case "create":
322                         err = arv.Create("collections",
323                                 Dict{"collection": Dict{"name": "testing"}},
324                                 &getback)
325                 case "update":
326                         err = arv.Update("collections", "zzzzz-4zz18-znfnqtbbv4spc3w",
327                                 Dict{"collection": Dict{"name": "testing"}},
328                                 &getback)
329                 case "delete":
330                         err = arv.Delete("pipeline_templates", "zzzzz-4zz18-znfnqtbbv4spc3w", nil, &getback)
331                 }
332
333                 switch stub.expected {
334                 case 200:
335                         c.Check(err, IsNil)
336                         c.Check(getback["ok"], Equals, "ok")
337                 case -1:
338                         c.Check(err, NotNil)
339                         c.Check(err, ErrorMatches, `.*stopped after \d+ redirects`)
340                 default:
341                         c.Check(err, NotNil)
342                         c.Check(err, ErrorMatches, fmt.Sprintf("arvados API server error: %d.*", stub.expected))
343                         c.Check(err.(APIServerError).HttpStatusCode, Equals, stub.expected)
344                 }
345         }
346 }