go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/metadata/legacy_test.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package metadata
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"testing"
    22  	"time"
    23  
    24  	"google.golang.org/protobuf/proto"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/gae/impl/memory"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  
    30  	api "go.chromium.org/luci/cipd/api/cipd/v1"
    31  
    32  	. "github.com/smartystreets/goconvey/convey"
    33  
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func TestLegacyMetadata(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	Convey("With legacy entities", t, func() {
    41  		impl := legacyStorageImpl{}
    42  
    43  		ctx := memory.Use(context.Background())
    44  		ts := time.Unix(1525136124, 0).UTC()
    45  
    46  		root := rootKey(ctx)
    47  		So(datastore.Put(ctx, []*packageACL{
    48  			// ACLs for "a".
    49  			{
    50  				ID:         "OWNER:a",
    51  				Parent:     root,
    52  				Users:      []string{"user:a-owner@example.com"},
    53  				Groups:     []string{"a-owner"},
    54  				ModifiedBy: "user:a-owner-mod@example.com",
    55  				ModifiedTS: ts,
    56  			},
    57  			{
    58  				ID:         "WRITER:a",
    59  				Parent:     root,
    60  				Users:      []string{"user:a-writer@example.com"},
    61  				Groups:     []string{"a-writer"},
    62  				ModifiedBy: "user:a-writer-mod@example.com",
    63  				ModifiedTS: ts.Add(5 * time.Second),
    64  			},
    65  			{
    66  				ID:         "READER:a",
    67  				Parent:     root,
    68  				Users:      []string{"user:a-reader@example.com"},
    69  				Groups:     []string{"a-reader"},
    70  				ModifiedBy: "user:a-reader-mod@example.com",
    71  				ModifiedTS: ts,
    72  			},
    73  
    74  			// Empty ACLs for "a/b".
    75  			{
    76  				ID:         "OWNER:a/b",
    77  				Parent:     root,
    78  				ModifiedBy: "user:b-owner-mod@example.com",
    79  				ModifiedTS: ts,
    80  				// no Users or Groups here
    81  			},
    82  
    83  			// ACLs for "a/b/c/d".
    84  			{
    85  				ID:         "OWNER:a/b/c/d",
    86  				Parent:     root,
    87  				Users:      []string{"user:d-owner@example.com", "bad:ident"},
    88  				Groups:     []string{"d-owner"},
    89  				ModifiedBy: "user:d-owner-mod@example.com",
    90  				ModifiedTS: ts,
    91  			},
    92  		}), ShouldBeNil)
    93  
    94  		rootMeta := rootMetadata()
    95  
    96  		// Expected metadata per prefix.
    97  		expected := map[string]*api.PrefixMetadata{
    98  			"a": {
    99  				Prefix:      "a",
   100  				Fingerprint: "BK-o5e-PimWmXtF3zdzvjiyAqSU",
   101  				UpdateTime:  timestamppb.New(ts.Add(5 * time.Second)), // WRITER:a mod time
   102  				UpdateUser:  "user:a-writer-mod@example.com",
   103  				Acls: []*api.PrefixMetadata_ACL{
   104  					{Role: api.Role_OWNER, Principals: []string{"user:a-owner@example.com", "group:a-owner"}},
   105  					{Role: api.Role_WRITER, Principals: []string{"user:a-writer@example.com", "group:a-writer"}},
   106  					{Role: api.Role_READER, Principals: []string{"user:a-reader@example.com", "group:a-reader"}},
   107  				},
   108  			},
   109  			"a/b": {
   110  				Prefix:      "a/b",
   111  				Fingerprint: "RyIXeT0HBpfv5Lj8FLqMzCu60ZI",
   112  				UpdateTime:  timestamppb.New(ts),
   113  				UpdateUser:  "user:b-owner-mod@example.com",
   114  			},
   115  			"a/b/c/d": {
   116  				Prefix:      "a/b/c/d",
   117  				Fingerprint: "4B97z37yN22RnBHS336ROctEC2w",
   118  				UpdateTime:  timestamppb.New(ts),
   119  				UpdateUser:  "user:d-owner-mod@example.com",
   120  				Acls: []*api.PrefixMetadata_ACL{
   121  					// Note: bad:ident is skipped here.
   122  					{Role: api.Role_OWNER, Principals: []string{"user:d-owner@example.com", "group:d-owner"}},
   123  				},
   124  			},
   125  		}
   126  
   127  		Convey("GetMetadata returns root metadata which has fingerprint", func() {
   128  			md, err := impl.GetMetadata(ctx, "")
   129  			So(err, ShouldBeNil)
   130  			So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta})
   131  			So(rootMeta, ShouldResembleProto, &api.PrefixMetadata{
   132  				Acls: []*api.PrefixMetadata_ACL{
   133  					{
   134  						Role:       api.Role_OWNER,
   135  						Principals: []string{"group:administrators"},
   136  					},
   137  				},
   138  				Fingerprint: "G7Hov8WrEwWHx1dQd7SMsKJERUI",
   139  			})
   140  		})
   141  
   142  		Convey("GetMetadata handles one prefix", func() {
   143  			md, err := impl.GetMetadata(ctx, "a")
   144  			So(err, ShouldBeNil)
   145  			So(md, ShouldResembleProto, []*api.PrefixMetadata{
   146  				rootMeta,
   147  				expected["a"],
   148  			})
   149  		})
   150  
   151  		Convey("GetMetadata handles many prefixes", func() {
   152  			// Returns only existing metadata, silently skipping undefined.
   153  			md, err := impl.GetMetadata(ctx, "a/b/c/d/e/")
   154  			So(err, ShouldBeNil)
   155  			So(md, ShouldResembleProto, []*api.PrefixMetadata{
   156  				rootMeta,
   157  				expected["a"],
   158  				expected["a/b"],
   159  				expected["a/b/c/d"],
   160  			})
   161  		})
   162  
   163  		Convey("GetMetadata handles root metadata", func() {
   164  			md, err := impl.GetMetadata(ctx, "")
   165  			So(err, ShouldBeNil)
   166  			So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta})
   167  		})
   168  
   169  		Convey("GetMetadata fails on bad prefix", func() {
   170  			_, err := impl.GetMetadata(ctx, "???")
   171  			So(err, ShouldErrLike, "invalid package prefix")
   172  		})
   173  
   174  		Convey("UpdateMetadata noop call with existing metadata", func() {
   175  			updated, err := impl.UpdateMetadata(ctx, "a", func(_ context.Context, md *api.PrefixMetadata) error {
   176  				So(md, ShouldResembleProto, expected["a"])
   177  				return nil
   178  			})
   179  			So(err, ShouldBeNil)
   180  			So(updated, ShouldResembleProto, expected["a"])
   181  		})
   182  
   183  		Convey("UpdateMetadata refuses to update root metadata", func() {
   184  			_, err := impl.UpdateMetadata(ctx, "", func(_ context.Context, md *api.PrefixMetadata) error {
   185  				panic("must not be called")
   186  			})
   187  			So(err, ShouldErrLike, "the root metadata is not modifiable")
   188  		})
   189  
   190  		Convey("UpdateMetadata updates existing metadata", func() {
   191  			modTime := ts.Add(10 * time.Second)
   192  
   193  			newMD := proto.Clone(expected["a"]).(*api.PrefixMetadata)
   194  			newMD.UpdateTime = timestamppb.New(modTime)
   195  			newMD.UpdateUser = "user:updater@example.com"
   196  			newMD.Acls[0].Principals = []string{
   197  				"group:new-owning-group",
   198  				"user:new-owner@example.com",
   199  				"group:another-group",
   200  			}
   201  
   202  			updated, err := impl.UpdateMetadata(ctx, "a", func(_ context.Context, md *api.PrefixMetadata) error {
   203  				So(md, ShouldResembleProto, expected["a"])
   204  				*md = *newMD
   205  				return nil
   206  			})
   207  			So(err, ShouldBeNil)
   208  
   209  			// The returned metadata is different from newMD: order of principals is
   210  			// not preserved, and the fingerprint is populated.
   211  			newMD.Acls[0].Principals = []string{
   212  				"user:new-owner@example.com",
   213  				"group:new-owning-group",
   214  				"group:another-group",
   215  			}
   216  			newMD.Fingerprint = "MCRIAGe9tfXGxAZ-mTQbjQiJAlA" // new FP
   217  			So(updated, ShouldResembleProto, newMD)
   218  
   219  			// GetMetadata sees the new metadata.
   220  			md, err := impl.GetMetadata(ctx, "a")
   221  			So(err, ShouldBeNil)
   222  			So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, newMD})
   223  
   224  			// Only touched "OWNER:..." legacy entity, since only owners changed.
   225  			legacy := prefixACLs(ctx, "a", nil)
   226  			So(datastore.Get(ctx, legacy), ShouldBeNil)
   227  			So(legacy, ShouldResemble, []*packageACL{
   228  				{
   229  					ID:         "OWNER:a",
   230  					Parent:     root,
   231  					Users:      []string{"user:new-owner@example.com"},
   232  					Groups:     []string{"new-owning-group", "another-group"},
   233  					ModifiedBy: "user:updater@example.com",
   234  					ModifiedTS: modTime,
   235  					Rev:        1,
   236  				},
   237  				// Untouched.
   238  				{
   239  					ID:         "WRITER:a",
   240  					Parent:     root,
   241  					Users:      []string{"user:a-writer@example.com"},
   242  					Groups:     []string{"a-writer"},
   243  					ModifiedBy: "user:a-writer-mod@example.com",
   244  					ModifiedTS: ts.Add(5 * time.Second),
   245  				},
   246  				// Untouched.
   247  				{
   248  					ID:         "READER:a",
   249  					Parent:     root,
   250  					Users:      []string{"user:a-reader@example.com"},
   251  					Groups:     []string{"a-reader"},
   252  					ModifiedBy: "user:a-reader-mod@example.com",
   253  					ModifiedTS: ts,
   254  				},
   255  			})
   256  		})
   257  
   258  		Convey("UpdateMetadata noop call with missing metadata", func() {
   259  			updated, err := impl.UpdateMetadata(ctx, "z", func(_ context.Context, md *api.PrefixMetadata) error {
   260  				So(md, ShouldResembleProto, &api.PrefixMetadata{Prefix: "z"})
   261  				return nil
   262  			})
   263  			So(err, ShouldBeNil)
   264  			So(updated, ShouldBeNil)
   265  
   266  			// Still missing.
   267  			md, err := impl.GetMetadata(ctx, "z")
   268  			So(err, ShouldBeNil)
   269  			So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta})
   270  		})
   271  
   272  		Convey("UpdateMetadata creates new metadata", func() {
   273  			updated, err := impl.UpdateMetadata(ctx, "z", func(_ context.Context, md *api.PrefixMetadata) error {
   274  				So(md, ShouldResembleProto, &api.PrefixMetadata{Prefix: "z"})
   275  				md.UpdateTime = timestamppb.New(ts)
   276  				md.UpdateUser = "user:updater@example.com"
   277  				md.Acls = []*api.PrefixMetadata_ACL{
   278  					{
   279  						Role: api.Role_READER,
   280  					},
   281  					{
   282  						Role:       api.Role_WRITER,
   283  						Principals: []string{"group:a", "user:a@example.com"},
   284  					},
   285  					{
   286  						Role:       api.Role_OWNER,
   287  						Principals: []string{"group:b"},
   288  					},
   289  				}
   290  				return nil
   291  			})
   292  			So(err, ShouldBeNil)
   293  
   294  			// Changes compared to what was stored in the callback:
   295  			//  * Acls are ordered by Role now.
   296  			//  * READER is missing, the principals list was empty.
   297  			//  * Principals are sorted by "users first, then groups".
   298  			expected := &api.PrefixMetadata{
   299  				Prefix:      "z",
   300  				Fingerprint: "ppDqWKGcl8Pu1hMiXQ1hac0vAH0",
   301  				UpdateTime:  timestamppb.New(ts),
   302  				UpdateUser:  "user:updater@example.com",
   303  				Acls: []*api.PrefixMetadata_ACL{
   304  					{
   305  						Role:       api.Role_OWNER,
   306  						Principals: []string{"group:b"},
   307  					},
   308  					{
   309  						Role:       api.Role_WRITER,
   310  						Principals: []string{"user:a@example.com", "group:a"},
   311  					},
   312  				},
   313  			}
   314  			So(updated, ShouldResembleProto, expected)
   315  
   316  			// Stored indeed.
   317  			md, err := impl.GetMetadata(ctx, "z")
   318  			So(err, ShouldBeNil)
   319  			So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, expected})
   320  		})
   321  
   322  		Convey("UpdateMetadata call with failing callback", func() {
   323  			cbErr := errors.New("blah")
   324  			updated, err := impl.UpdateMetadata(ctx, "z", func(_ context.Context, md *api.PrefixMetadata) error {
   325  				md.UpdateUser = "user:must-be-ignored@example.com"
   326  				return cbErr
   327  			})
   328  			So(err, ShouldEqual, cbErr) // exact same error object
   329  			So(updated, ShouldBeNil)
   330  
   331  			// Still missing.
   332  			md, err := impl.GetMetadata(ctx, "z")
   333  			So(err, ShouldBeNil)
   334  			So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta})
   335  		})
   336  	})
   337  }
   338  
   339  func TestVisitMetadata(t *testing.T) {
   340  	t.Parallel()
   341  
   342  	Convey("With datastore", t, func() {
   343  		ctx := memory.Use(context.Background())
   344  		ts := time.Unix(1525136124, 0).UTC()
   345  
   346  		impl := legacyStorageImpl{}
   347  
   348  		add := func(role, pfx, group string) {
   349  			So(datastore.Put(ctx, &packageACL{
   350  				ID:         role + ":" + pfx,
   351  				Parent:     rootKey(ctx),
   352  				ModifiedTS: ts,
   353  				Groups:     []string{group},
   354  			}), ShouldBeNil)
   355  		}
   356  
   357  		type visited struct {
   358  			prefix string
   359  			md     []string // pfx:role:principal, sorted
   360  		}
   361  
   362  		visit := func(pfx string) (res []visited) {
   363  			err := impl.VisitMetadata(ctx, pfx, func(p string, md []*api.PrefixMetadata) (bool, error) {
   364  				extract := []string{}
   365  				for _, m := range md {
   366  					for _, acl := range m.Acls {
   367  						for _, p := range acl.Principals {
   368  							extract = append(extract, fmt.Sprintf("%s:%s:%s", m.Prefix, acl.Role, p))
   369  						}
   370  					}
   371  				}
   372  				res = append(res, visited{p, extract})
   373  				return true, nil
   374  			})
   375  			So(err, ShouldBeNil)
   376  			return
   377  		}
   378  
   379  		add("OWNER", "a", "o-a")
   380  		add("READER", "a", "r-a")
   381  
   382  		add("OWNER", "a/b/c", "o-abc")
   383  		add("READER", "a/b/c", "r-abc")
   384  		add("WRITER", "a/b/c", "w-abc")
   385  
   386  		add("READER", "a/b/c/d", "r-abcd")
   387  
   388  		add("OWNER", "ab", "o-ab")
   389  		add("READER", "ab", "r-ab")
   390  
   391  		Convey("Root listing", func() {
   392  			So(visit(""), ShouldResemble, []visited{
   393  				{
   394  					"", []string{
   395  						":OWNER:group:administrators",
   396  					},
   397  				},
   398  				{
   399  					"a", []string{
   400  						":OWNER:group:administrators",
   401  						"a:OWNER:group:o-a",
   402  						"a:READER:group:r-a",
   403  					},
   404  				},
   405  				{
   406  					"a/b/c", []string{
   407  						":OWNER:group:administrators",
   408  						"a:OWNER:group:o-a",
   409  						"a:READER:group:r-a",
   410  						"a/b/c:OWNER:group:o-abc",
   411  						"a/b/c:WRITER:group:w-abc",
   412  						"a/b/c:READER:group:r-abc",
   413  					},
   414  				},
   415  				{
   416  					"a/b/c/d", []string{
   417  						":OWNER:group:administrators",
   418  						"a:OWNER:group:o-a",
   419  						"a:READER:group:r-a",
   420  						"a/b/c:OWNER:group:o-abc",
   421  						"a/b/c:WRITER:group:w-abc",
   422  						"a/b/c:READER:group:r-abc",
   423  						"a/b/c/d:READER:group:r-abcd",
   424  					},
   425  				},
   426  				{
   427  					"ab", []string{
   428  						":OWNER:group:administrators",
   429  						"ab:OWNER:group:o-ab",
   430  						"ab:READER:group:r-ab",
   431  					},
   432  				},
   433  			})
   434  		})
   435  
   436  		Convey("Prefix listing", func() {
   437  			So(visit("a"), ShouldResemble, []visited{
   438  				{
   439  					"a", []string{
   440  						":OWNER:group:administrators",
   441  						"a:OWNER:group:o-a",
   442  						"a:READER:group:r-a",
   443  					},
   444  				},
   445  				{
   446  					"a/b/c", []string{
   447  						":OWNER:group:administrators",
   448  						"a:OWNER:group:o-a",
   449  						"a:READER:group:r-a",
   450  						"a/b/c:OWNER:group:o-abc",
   451  						"a/b/c:WRITER:group:w-abc",
   452  						"a/b/c:READER:group:r-abc",
   453  					},
   454  				},
   455  				{
   456  					"a/b/c/d", []string{
   457  						":OWNER:group:administrators",
   458  						"a:OWNER:group:o-a",
   459  						"a:READER:group:r-a",
   460  						"a/b/c:OWNER:group:o-abc",
   461  						"a/b/c:WRITER:group:w-abc",
   462  						"a/b/c:READER:group:r-abc",
   463  						"a/b/c/d:READER:group:r-abcd",
   464  					},
   465  				},
   466  			})
   467  		})
   468  
   469  		Convey("Missing prefix listing", func() {
   470  			So(visit("z/z/z"), ShouldResemble, []visited{
   471  				{
   472  					"z/z/z", []string{":OWNER:group:administrators"},
   473  				},
   474  			})
   475  		})
   476  
   477  		Convey("Callback return value is respected, stopping right away", func() {
   478  			seen := []string{}
   479  			err := impl.VisitMetadata(ctx, "a", func(p string, md []*api.PrefixMetadata) (bool, error) {
   480  				seen = append(seen, p)
   481  				return false, nil
   482  			})
   483  			So(err, ShouldBeNil)
   484  			So(seen, ShouldResemble, []string{"a"})
   485  		})
   486  
   487  		Convey("Callback return value is respected, stopping later", func() {
   488  			seen := []string{}
   489  			err := impl.VisitMetadata(ctx, "a", func(p string, md []*api.PrefixMetadata) (bool, error) {
   490  				seen = append(seen, p)
   491  				return p != "a/b/c", nil
   492  			})
   493  			So(err, ShouldBeNil)
   494  			So(seen, ShouldResemble, []string{"a", "a/b/c"}) // no a/b/c/d
   495  		})
   496  	})
   497  }
   498  
   499  func TestParseKey(t *testing.T) {
   500  	t.Parallel()
   501  
   502  	cases := []struct {
   503  		id     string
   504  		role   string
   505  		prefix string
   506  		err    string
   507  	}{
   508  		{"OWNER:a/b/c", "OWNER", "a/b/c", ""},
   509  		{"OWNER", "", "", "not <role>:<prefix> pair"},
   510  		{"UNKNOWN:a/b/c", "", "", "unrecognized role"},
   511  		{"OWNER:///", "", "", "invalid package prefix"},
   512  		{"OWNER:", "", "", "invalid package prefix"},
   513  	}
   514  
   515  	for _, c := range cases {
   516  		Convey(fmt.Sprintf("works for %q", c.id), t, func() {
   517  			role, pfx, err := (&packageACL{ID: c.id}).parseKey()
   518  			So(role, ShouldEqual, c.role)
   519  			So(pfx, ShouldEqual, c.prefix)
   520  			if c.err == "" {
   521  				So(err, ShouldBeNil)
   522  			} else {
   523  				So(err, ShouldErrLike, c.err)
   524  			}
   525  		})
   526  	}
   527  }
   528  
   529  func TestListACLsByPrefix(t *testing.T) {
   530  	t.Parallel()
   531  
   532  	Convey("With datastore", t, func() {
   533  		ctx := memory.Use(context.Background())
   534  
   535  		add := func(role, pfx string) {
   536  			So(datastore.Put(ctx, &packageACL{
   537  				ID:     role + ":" + pfx,
   538  				Parent: rootKey(ctx),
   539  				Groups: []string{"blah"}, //  to make sure bodies are fetched too
   540  			}), ShouldBeNil)
   541  		}
   542  
   543  		list := func(role, pfx string) (out []string) {
   544  			acls, err := listACLsByPrefix(ctx, role, pfx)
   545  			So(err, ShouldBeNil)
   546  			for _, acl := range acls {
   547  				So(acl.Groups, ShouldResemble, []string{"blah"})
   548  				out = append(out, acl.ID)
   549  			}
   550  			return
   551  		}
   552  
   553  		add("OWNER", "a")
   554  		add("OWNER", "a/b/c")
   555  		add("OWNER", "ab")
   556  		add("READER", "a")
   557  		add("READER", "a/b/c")
   558  		add("READER", "a/b/c/d")
   559  		add("READER", "ab")
   560  
   561  		Convey("Root listing", func() {
   562  			So(list("OWNER", ""), ShouldResemble, []string{
   563  				"OWNER:a", "OWNER:a/b/c", "OWNER:ab",
   564  			})
   565  			So(list("READER", ""), ShouldResemble, []string{
   566  				"READER:a", "READER:a/b/c", "READER:a/b/c/d", "READER:ab",
   567  			})
   568  			So(list("WRITER", ""), ShouldResemble, []string(nil))
   569  		})
   570  
   571  		Convey("Non-root listing", func() {
   572  			So(list("OWNER", "a"), ShouldResemble, []string{"OWNER:a/b/c"})
   573  			So(list("READER", "a"), ShouldResemble, []string{"READER:a/b/c", "READER:a/b/c/d"})
   574  			So(list("WRITER", "a"), ShouldResemble, []string(nil))
   575  		})
   576  
   577  		Convey("Non-existing prefix listing", func() {
   578  			So(list("OWNER", "z"), ShouldResemble, []string(nil))
   579  		})
   580  	})
   581  }
   582  
   583  func TestMetadataGraph(t *testing.T) {
   584  	t.Parallel()
   585  
   586  	Convey("With metadataGraph", t, func() {
   587  		ctx := memory.Use(context.Background())
   588  		ts := time.Unix(1525136124, 0).UTC()
   589  
   590  		gr := metadataGraph{}
   591  		gr.init(&api.PrefixMetadata{
   592  			Acls: []*api.PrefixMetadata_ACL{
   593  				{
   594  					Role:       api.Role_OWNER,
   595  					Principals: []string{"group:root"},
   596  				},
   597  			},
   598  		})
   599  
   600  		insert := func(role, prefix, group string) {
   601  			gr.insert(ctx, []*packageACL{
   602  				{
   603  					ID:         role + ":" + prefix,
   604  					Parent:     rootKey(ctx),
   605  					Groups:     []string{group},
   606  					ModifiedTS: ts, // to mark as non-empty
   607  				},
   608  			})
   609  		}
   610  
   611  		type visited struct {
   612  			prefix string
   613  			md     []string // pfx:role:principal, sorted
   614  		}
   615  
   616  		freezeAndVisit := func(node string) (v []visited) {
   617  			n := gr.node(node)
   618  			gr.freeze(ctx)
   619  
   620  			err := n.traverse(nil, func(n *metadataNode, md []*api.PrefixMetadata) (bool, error) {
   621  				extract := []string{}
   622  				for _, m := range md {
   623  					for _, acl := range m.Acls {
   624  						for _, p := range acl.Principals {
   625  							extract = append(extract, fmt.Sprintf("%s:%s:%s", m.Prefix, acl.Role, p))
   626  						}
   627  					}
   628  				}
   629  				v = append(v, visited{n.prefix, extract})
   630  				return true, nil
   631  			})
   632  			So(err, ShouldBeNil)
   633  			return
   634  		}
   635  
   636  		insert("OWNER", "a/b/c/d", "owner-abc")
   637  		insert("OWNER", "a", "owner-a")
   638  		insert("OWNER", "b", "owner-b")
   639  		insert("READER", "a", "reader-a")
   640  		insert("READER", "a/b", "reader-ab")
   641  		insert("READER", "a/bc", "reader-abc")
   642  		insert("BOGUS", "a/b", "bogus-ab")
   643  
   644  		Convey("Traverse from the root", func() {
   645  			So(freezeAndVisit(""), ShouldResemble, []visited{
   646  				{"", []string{":OWNER:group:root"}},
   647  				{
   648  					"a", []string{
   649  						":OWNER:group:root",
   650  						"a:OWNER:group:owner-a",
   651  						"a:READER:group:reader-a",
   652  					},
   653  				},
   654  				{
   655  					"a/b", []string{
   656  						":OWNER:group:root",
   657  						"a:OWNER:group:owner-a",
   658  						"a:READER:group:reader-a",
   659  						"a/b:READER:group:reader-ab",
   660  					},
   661  				},
   662  				{
   663  					"a/b/c", []string{
   664  						":OWNER:group:root",
   665  						"a:OWNER:group:owner-a",
   666  						"a:READER:group:reader-a",
   667  						"a/b:READER:group:reader-ab",
   668  					},
   669  				},
   670  				{
   671  					"a/b/c/d", []string{
   672  						":OWNER:group:root",
   673  						"a:OWNER:group:owner-a",
   674  						"a:READER:group:reader-a",
   675  						"a/b:READER:group:reader-ab",
   676  						"a/b/c/d:OWNER:group:owner-abc",
   677  					},
   678  				},
   679  				{
   680  					"a/bc", []string{
   681  						":OWNER:group:root",
   682  						"a:OWNER:group:owner-a",
   683  						"a:READER:group:reader-a",
   684  						"a/bc:READER:group:reader-abc",
   685  					},
   686  				},
   687  				{
   688  					"b", []string{
   689  						":OWNER:group:root",
   690  						"b:OWNER:group:owner-b",
   691  					},
   692  				},
   693  			})
   694  		})
   695  
   696  		Convey("Traverse from some prefix", func() {
   697  			So(freezeAndVisit("a/b"), ShouldResemble, []visited{
   698  				{
   699  					"a/b", []string{
   700  						":OWNER:group:root",
   701  						"a:OWNER:group:owner-a",
   702  						"a:READER:group:reader-a",
   703  						"a/b:READER:group:reader-ab",
   704  					},
   705  				},
   706  				{
   707  					"a/b/c", []string{
   708  						":OWNER:group:root",
   709  						"a:OWNER:group:owner-a",
   710  						"a:READER:group:reader-a",
   711  						"a/b:READER:group:reader-ab",
   712  					},
   713  				},
   714  				{
   715  					"a/b/c/d", []string{
   716  						":OWNER:group:root",
   717  						"a:OWNER:group:owner-a",
   718  						"a:READER:group:reader-a",
   719  						"a/b:READER:group:reader-ab",
   720  						"a/b/c/d:OWNER:group:owner-abc",
   721  					},
   722  				},
   723  			})
   724  		})
   725  
   726  		Convey("Traverse from some deep prefix", func() {
   727  			So(freezeAndVisit("a/b/c/d/e"), ShouldResemble, []visited{
   728  				{
   729  					"a/b/c/d/e", []string{
   730  						":OWNER:group:root",
   731  						"a:OWNER:group:owner-a",
   732  						"a:READER:group:reader-a",
   733  						"a/b:READER:group:reader-ab",
   734  						"a/b/c/d:OWNER:group:owner-abc",
   735  					},
   736  				},
   737  			})
   738  		})
   739  
   740  		Convey("Traverse from some non-existing prefix", func() {
   741  			So(freezeAndVisit("z/z/z"), ShouldResemble, []visited{
   742  				{
   743  					"z/z/z", []string{":OWNER:group:root"},
   744  				},
   745  			})
   746  		})
   747  	})
   748  }