1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
15 "git.curoverse.com/arvados.git/lib/controller/router"
16 "git.curoverse.com/arvados.git/lib/controller/rpc"
17 "git.curoverse.com/arvados.git/sdk/go/arvados"
18 "git.curoverse.com/arvados.git/sdk/go/arvadostest"
19 "git.curoverse.com/arvados.git/sdk/go/auth"
20 "git.curoverse.com/arvados.git/sdk/go/ctxlog"
21 "git.curoverse.com/arvados.git/sdk/go/httpserver"
22 check "gopkg.in/check.v1"
25 // Gocheck boilerplate
26 func Test(t *testing.T) {
31 _ = check.Suite(&FederationSuite{})
32 _ = check.Suite(&CollectionListSuite{})
35 type FederationSuite struct {
36 cluster *arvados.Cluster
41 func (s *FederationSuite) SetUpTest(c *check.C) {
42 s.cluster = &arvados.Cluster{
44 RemoteClusters: map[string]arvados.RemoteCluster{
45 "aaaaa": arvados.RemoteCluster{
46 Host: os.Getenv("ARVADOS_API_HOST"),
50 arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
51 s.cluster.TLS.Insecure = true
52 s.cluster.API.MaxItemsPerResponse = 3
54 ctx := context.Background()
55 ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
56 ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
59 s.fed = New(s.cluster)
62 func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend) {
63 s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
64 Host: "in-process.local",
66 s.fed.remotes[id] = backend
69 func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
70 srv := httpserver.Server{Addr: ":"}
71 srv.Handler = router.New(backend)
72 c.Check(srv.Start(), check.IsNil)
73 s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
77 s.fed.remotes[id] = rpc.NewConn(id, &url.URL{Scheme: "http", Host: srv.Addr}, true, saltedTokenProvider(s.fed.local, id))
80 type collectionLister struct {
82 ItemsToReturn []arvados.Collection
86 func (cl *collectionLister) matchFilters(c arvados.Collection, filters []arvados.Filter) bool {
88 for _, f := range filters {
89 if f.Attr == "uuid" && f.Operator == "=" {
90 s, ok := f.Operand.(string)
91 if ok && s == c.UUID {
94 } else if f.Attr == "uuid" && f.Operator == "in" {
95 if operand, ok := f.Operand.([]string); ok {
96 for _, s := range operand {
101 } else if operand, ok := f.Operand.([]interface{}); ok {
102 for _, s := range operand {
103 if s, ok := s.(string); ok && s == c.UUID {
114 func (cl *collectionLister) CollectionList(ctx context.Context, options arvados.ListOptions) (resp arvados.CollectionList, _ error) {
115 cl.APIStub.CollectionList(ctx, options)
116 for _, c := range cl.ItemsToReturn {
117 if cl.MaxPageSize > 0 && len(resp.Items) >= cl.MaxPageSize {
120 if options.Limit >= 0 && len(resp.Items) >= options.Limit {
123 if cl.matchFilters(c, options.Filters) {
124 resp.Items = append(resp.Items, c)
130 type CollectionListSuite struct {
132 ids []string // aaaaa, bbbbb, ccccc
133 uuids [][]string // [[aa-*, aa-*, aa-*], [bb-*, bb-*, ...], ...]
134 backends []*collectionLister
137 func (s *CollectionListSuite) SetUpTest(c *check.C) {
138 s.FederationSuite.SetUpTest(c)
143 for i, id := range []string{"aaaaa", "bbbbb", "ccccc"} {
144 cl := &collectionLister{}
145 s.ids = append(s.ids, id)
146 s.uuids = append(s.uuids, nil)
147 for j := 0; j < 5; j++ {
148 uuid := fmt.Sprintf("%s-4zz18-%s%010d", id, id, j)
149 s.uuids[i] = append(s.uuids[i], uuid)
150 cl.ItemsToReturn = append(cl.ItemsToReturn, arvados.Collection{
154 s.backends = append(s.backends, cl)
158 // call some backends directly via API
159 s.addDirectRemote(c, id, cl)
161 // call some backends through rpc->router->API
162 // to ensure nothing is lost in translation
163 s.addHTTPRemote(c, id, cl)
168 type listTrial struct {
173 filters []arvados.Filter
175 expectCalls []int // number of API calls to backends
179 func (s *CollectionListSuite) TestCollectionListNoUUIDFilters(c *check.C) {
183 expectUUIDs: []string{s.uuids[0][0]},
184 expectCalls: []int{1, 0, 0},
188 func (s *CollectionListSuite) TestCollectionListOneLocal(c *check.C) {
192 filters: []arvados.Filter{{"uuid", "=", s.uuids[0][0]}},
193 expectUUIDs: []string{s.uuids[0][0]},
194 expectCalls: []int{1, 0, 0},
198 func (s *CollectionListSuite) TestCollectionListOneRemote(c *check.C) {
202 filters: []arvados.Filter{{"uuid", "=", s.uuids[1][0]}},
203 expectUUIDs: []string{s.uuids[1][0]},
204 expectCalls: []int{0, 1, 0},
208 func (s *CollectionListSuite) TestCollectionListOneLocalUsingInOperator(c *check.C) {
212 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0]}}},
213 expectUUIDs: []string{s.uuids[0][0]},
214 expectCalls: []int{1, 0, 0},
218 func (s *CollectionListSuite) TestCollectionListOneRemoteUsingInOperator(c *check.C) {
222 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[1][1]}}},
223 expectUUIDs: []string{s.uuids[1][1]},
224 expectCalls: []int{0, 1, 0},
228 func (s *CollectionListSuite) TestCollectionListOneLocalOneRemote(c *check.C) {
232 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}}},
233 expectUUIDs: []string{s.uuids[0][0], s.uuids[1][0]},
234 expectCalls: []int{1, 1, 0},
238 func (s *CollectionListSuite) TestCollectionListTwoRemotes(c *check.C) {
242 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[2][0], s.uuids[1][0]}}},
243 expectUUIDs: []string{s.uuids[1][0], s.uuids[2][0]},
244 expectCalls: []int{0, 1, 1},
248 func (s *CollectionListSuite) TestCollectionListSatisfyAllFilters(c *check.C) {
249 s.cluster.API.MaxItemsPerResponse = 2
253 filters: []arvados.Filter{
254 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][1], s.uuids[2][0], s.uuids[2][1], s.uuids[2][2]}},
255 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][2], s.uuids[2][1]}},
257 expectUUIDs: []string{s.uuids[0][0], s.uuids[2][1]},
258 expectCalls: []int{1, 0, 1},
262 func (s *CollectionListSuite) TestCollectionListEmptySet(c *check.C) {
266 filters: []arvados.Filter{{"uuid", "in", []string{}}},
267 expectUUIDs: []string{},
268 expectCalls: []int{0, 0, 0},
272 func (s *CollectionListSuite) TestCollectionListUnmatchableUUID(c *check.C) {
276 filters: []arvados.Filter{
277 {"uuid", "in", []string{s.uuids[0][0], "abcdefg"}},
278 {"uuid", "in", []string{s.uuids[0][0], "bbbbb-4zz18-bogus"}},
279 {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-bogus"}},
281 expectUUIDs: []string{s.uuids[0][0]},
282 expectCalls: []int{1, 0, 0},
286 func (s *CollectionListSuite) TestCollectionListMultiPage(c *check.C) {
287 for i := range s.backends {
288 s.uuids[i] = s.uuids[i][:3]
289 s.backends[i].ItemsToReturn = s.backends[i].ItemsToReturn[:3]
291 s.cluster.API.MaxItemsPerResponse = 9
292 for _, stub := range s.backends {
295 allUUIDs := append(append(append([]string(nil), s.uuids[0]...), s.uuids[1]...), s.uuids[2]...)
299 filters: []arvados.Filter{{"uuid", "in", append([]string(nil), allUUIDs...)}},
300 expectUUIDs: allUUIDs,
301 expectCalls: []int{2, 2, 2},
305 func (s *CollectionListSuite) TestCollectionListMultiSiteExtraFilters(c *check.C) {
306 // not [yet] supported
310 filters: []arvados.Filter{
311 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
312 {"uuid", "is_a", "teapot"},
314 expectCalls: []int{0, 0, 0},
315 expectStatus: http.StatusBadRequest,
319 func (s *CollectionListSuite) TestCollectionListMultiSiteWithCount(c *check.C) {
320 for _, count := range []string{"", "exact"} {
324 filters: []arvados.Filter{
325 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
326 {"uuid", "is_a", "teapot"},
328 expectCalls: []int{0, 0, 0},
329 expectStatus: http.StatusBadRequest,
334 func (s *CollectionListSuite) TestCollectionListMultiSiteWithLimit(c *check.C) {
335 for _, limit := range []int{0, 1, 2} {
339 filters: []arvados.Filter{
340 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
341 {"uuid", "is_a", "teapot"},
343 expectCalls: []int{0, 0, 0},
344 expectStatus: http.StatusBadRequest,
349 func (s *CollectionListSuite) TestCollectionListMultiSiteWithOffset(c *check.C) {
354 filters: []arvados.Filter{
355 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
356 {"uuid", "is_a", "teapot"},
358 expectCalls: []int{0, 0, 0},
359 expectStatus: http.StatusBadRequest,
363 func (s *CollectionListSuite) TestCollectionListMultiSiteWithOrder(c *check.C) {
367 order: []string{"uuid desc"},
368 filters: []arvados.Filter{
369 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
370 {"uuid", "is_a", "teapot"},
372 expectCalls: []int{0, 0, 0},
373 expectStatus: http.StatusBadRequest,
377 func (s *CollectionListSuite) TestCollectionListInvalidFilters(c *check.C) {
381 filters: []arvados.Filter{
382 {"uuid", "in", "teapot"},
384 expectCalls: []int{0, 0, 0},
385 expectStatus: http.StatusBadRequest,
389 func (s *CollectionListSuite) TestCollectionListRemoteUnknown(c *check.C) {
393 filters: []arvados.Filter{
394 {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-000001111122222"}},
396 expectStatus: http.StatusNotFound,
400 func (s *CollectionListSuite) TestCollectionListRemoteError(c *check.C) {
401 s.addDirectRemote(c, "bbbbb", &arvadostest.APIStub{})
405 filters: []arvados.Filter{
406 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
408 expectStatus: http.StatusBadGateway,
412 func (s *CollectionListSuite) test(c *check.C, trial listTrial) {
413 resp, err := s.fed.CollectionList(s.ctx, arvados.ListOptions{
416 Offset: trial.offset,
418 Filters: trial.filters,
420 if trial.expectStatus != 0 {
421 c.Assert(err, check.NotNil)
422 err, _ := err.(interface{ HTTPStatus() int })
423 c.Assert(err, check.NotNil) // err must implement HTTPStatus()
424 c.Check(err.HTTPStatus(), check.Equals, trial.expectStatus)
425 c.Logf("returned error is %#v", err)
426 c.Logf("returned error string is %q", err)
428 c.Check(err, check.IsNil)
429 var expectItems []arvados.Collection
430 for _, uuid := range trial.expectUUIDs {
431 expectItems = append(expectItems, arvados.Collection{UUID: uuid})
433 c.Check(resp, check.DeepEquals, arvados.CollectionList{
438 for i, stub := range s.backends {
439 if i >= len(trial.expectCalls) {
442 calls := stub.Calls(nil)
443 c.Check(calls, check.HasLen, trial.expectCalls[i])
447 opts := calls[0].Options.(arvados.ListOptions)
448 c.Check(opts.Limit, check.Equals, trial.limit)