| // Copyright 2018 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package metadata |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "testing" |
| "time" |
| |
| "google.golang.org/protobuf/proto" |
| "google.golang.org/protobuf/types/known/timestamppb" |
| |
| "go.chromium.org/luci/gae/impl/memory" |
| "go.chromium.org/luci/gae/service/datastore" |
| |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| |
| . "github.com/smartystreets/goconvey/convey" |
| |
| . "go.chromium.org/luci/common/testing/assertions" |
| ) |
| |
| func TestLegacyMetadata(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With legacy entities", t, func() { |
| impl := legacyStorageImpl{} |
| |
| ctx := memory.Use(context.Background()) |
| ts := time.Unix(1525136124, 0).UTC() |
| |
| root := rootKey(ctx) |
| So(datastore.Put(ctx, []*packageACL{ |
| // ACLs for "a". |
| { |
| ID: "OWNER:a", |
| Parent: root, |
| Users: []string{"user:a-owner@example.com"}, |
| Groups: []string{"a-owner"}, |
| ModifiedBy: "user:a-owner-mod@example.com", |
| ModifiedTS: ts, |
| }, |
| { |
| ID: "WRITER:a", |
| Parent: root, |
| Users: []string{"user:a-writer@example.com"}, |
| Groups: []string{"a-writer"}, |
| ModifiedBy: "user:a-writer-mod@example.com", |
| ModifiedTS: ts.Add(5 * time.Second), |
| }, |
| { |
| ID: "READER:a", |
| Parent: root, |
| Users: []string{"user:a-reader@example.com"}, |
| Groups: []string{"a-reader"}, |
| ModifiedBy: "user:a-reader-mod@example.com", |
| ModifiedTS: ts, |
| }, |
| |
| // Empty ACLs for "a/b". |
| { |
| ID: "OWNER:a/b", |
| Parent: root, |
| ModifiedBy: "user:b-owner-mod@example.com", |
| ModifiedTS: ts, |
| // no Users or Groups here |
| }, |
| |
| // ACLs for "a/b/c/d". |
| { |
| ID: "OWNER:a/b/c/d", |
| Parent: root, |
| Users: []string{"user:d-owner@example.com", "bad:ident"}, |
| Groups: []string{"d-owner"}, |
| ModifiedBy: "user:d-owner-mod@example.com", |
| ModifiedTS: ts, |
| }, |
| }), ShouldBeNil) |
| |
| rootMeta := rootMetadata() |
| |
| // Expected metadata per prefix. |
| expected := map[string]*api.PrefixMetadata{ |
| "a": { |
| Prefix: "a", |
| Fingerprint: "BK-o5e-PimWmXtF3zdzvjiyAqSU", |
| UpdateTime: timestamppb.New(ts.Add(5 * time.Second)), // WRITER:a mod time |
| UpdateUser: "user:a-writer-mod@example.com", |
| Acls: []*api.PrefixMetadata_ACL{ |
| {Role: api.Role_OWNER, Principals: []string{"user:a-owner@example.com", "group:a-owner"}}, |
| {Role: api.Role_WRITER, Principals: []string{"user:a-writer@example.com", "group:a-writer"}}, |
| {Role: api.Role_READER, Principals: []string{"user:a-reader@example.com", "group:a-reader"}}, |
| }, |
| }, |
| "a/b": { |
| Prefix: "a/b", |
| Fingerprint: "RyIXeT0HBpfv5Lj8FLqMzCu60ZI", |
| UpdateTime: timestamppb.New(ts), |
| UpdateUser: "user:b-owner-mod@example.com", |
| }, |
| "a/b/c/d": { |
| Prefix: "a/b/c/d", |
| Fingerprint: "4B97z37yN22RnBHS336ROctEC2w", |
| UpdateTime: timestamppb.New(ts), |
| UpdateUser: "user:d-owner-mod@example.com", |
| Acls: []*api.PrefixMetadata_ACL{ |
| // Note: bad:ident is skipped here. |
| {Role: api.Role_OWNER, Principals: []string{"user:d-owner@example.com", "group:d-owner"}}, |
| }, |
| }, |
| } |
| |
| Convey("GetMetadata returns root metadata which has fingerprint", func() { |
| md, err := impl.GetMetadata(ctx, "") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta}) |
| So(rootMeta, ShouldResembleProto, &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"group:administrators"}, |
| }, |
| }, |
| Fingerprint: "G7Hov8WrEwWHx1dQd7SMsKJERUI", |
| }) |
| }) |
| |
| Convey("GetMetadata handles one prefix", func() { |
| md, err := impl.GetMetadata(ctx, "a") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{ |
| rootMeta, |
| expected["a"], |
| }) |
| }) |
| |
| Convey("GetMetadata handles many prefixes", func() { |
| // Returns only existing metadata, silently skipping undefined. |
| md, err := impl.GetMetadata(ctx, "a/b/c/d/e/") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{ |
| rootMeta, |
| expected["a"], |
| expected["a/b"], |
| expected["a/b/c/d"], |
| }) |
| }) |
| |
| Convey("GetMetadata handles root metadata", func() { |
| md, err := impl.GetMetadata(ctx, "") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta}) |
| }) |
| |
| Convey("GetMetadata fails on bad prefix", func() { |
| _, err := impl.GetMetadata(ctx, "???") |
| So(err, ShouldErrLike, "invalid package prefix") |
| }) |
| |
| Convey("UpdateMetadata noop call with existing metadata", func() { |
| updated, err := impl.UpdateMetadata(ctx, "a", func(_ context.Context, md *api.PrefixMetadata) error { |
| So(md, ShouldResembleProto, expected["a"]) |
| return nil |
| }) |
| So(err, ShouldBeNil) |
| So(updated, ShouldResembleProto, expected["a"]) |
| }) |
| |
| Convey("UpdateMetadata refuses to update root metadata", func() { |
| _, err := impl.UpdateMetadata(ctx, "", func(_ context.Context, md *api.PrefixMetadata) error { |
| panic("must not be called") |
| }) |
| So(err, ShouldErrLike, "the root metadata is not modifiable") |
| }) |
| |
| Convey("UpdateMetadata updates existing metadata", func() { |
| modTime := ts.Add(10 * time.Second) |
| |
| newMD := proto.Clone(expected["a"]).(*api.PrefixMetadata) |
| newMD.UpdateTime = timestamppb.New(modTime) |
| newMD.UpdateUser = "user:updater@example.com" |
| newMD.Acls[0].Principals = []string{ |
| "group:new-owning-group", |
| "user:new-owner@example.com", |
| "group:another-group", |
| } |
| |
| updated, err := impl.UpdateMetadata(ctx, "a", func(_ context.Context, md *api.PrefixMetadata) error { |
| So(md, ShouldResembleProto, expected["a"]) |
| *md = *newMD |
| return nil |
| }) |
| So(err, ShouldBeNil) |
| |
| // The returned metadata is different from newMD: order of principals is |
| // not preserved, and the fingerprint is populated. |
| newMD.Acls[0].Principals = []string{ |
| "user:new-owner@example.com", |
| "group:new-owning-group", |
| "group:another-group", |
| } |
| newMD.Fingerprint = "MCRIAGe9tfXGxAZ-mTQbjQiJAlA" // new FP |
| So(updated, ShouldResembleProto, newMD) |
| |
| // GetMetadata sees the new metadata. |
| md, err := impl.GetMetadata(ctx, "a") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, newMD}) |
| |
| // Only touched "OWNER:..." legacy entity, since only owners changed. |
| legacy := prefixACLs(ctx, "a", nil) |
| So(datastore.Get(ctx, legacy), ShouldBeNil) |
| So(legacy, ShouldResemble, []*packageACL{ |
| { |
| ID: "OWNER:a", |
| Parent: root, |
| Users: []string{"user:new-owner@example.com"}, |
| Groups: []string{"new-owning-group", "another-group"}, |
| ModifiedBy: "user:updater@example.com", |
| ModifiedTS: modTime, |
| Rev: 1, |
| }, |
| // Untouched. |
| { |
| ID: "WRITER:a", |
| Parent: root, |
| Users: []string{"user:a-writer@example.com"}, |
| Groups: []string{"a-writer"}, |
| ModifiedBy: "user:a-writer-mod@example.com", |
| ModifiedTS: ts.Add(5 * time.Second), |
| }, |
| // Untouched. |
| { |
| ID: "READER:a", |
| Parent: root, |
| Users: []string{"user:a-reader@example.com"}, |
| Groups: []string{"a-reader"}, |
| ModifiedBy: "user:a-reader-mod@example.com", |
| ModifiedTS: ts, |
| }, |
| }) |
| }) |
| |
| Convey("UpdateMetadata noop call with missing metadata", func() { |
| updated, err := impl.UpdateMetadata(ctx, "z", func(_ context.Context, md *api.PrefixMetadata) error { |
| So(md, ShouldResembleProto, &api.PrefixMetadata{Prefix: "z"}) |
| return nil |
| }) |
| So(err, ShouldBeNil) |
| So(updated, ShouldBeNil) |
| |
| // Still missing. |
| md, err := impl.GetMetadata(ctx, "z") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta}) |
| }) |
| |
| Convey("UpdateMetadata creates new metadata", func() { |
| updated, err := impl.UpdateMetadata(ctx, "z", func(_ context.Context, md *api.PrefixMetadata) error { |
| So(md, ShouldResembleProto, &api.PrefixMetadata{Prefix: "z"}) |
| md.UpdateTime = timestamppb.New(ts) |
| md.UpdateUser = "user:updater@example.com" |
| md.Acls = []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| }, |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"group:a", "user:a@example.com"}, |
| }, |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"group:b"}, |
| }, |
| } |
| return nil |
| }) |
| So(err, ShouldBeNil) |
| |
| // Changes compared to what was stored in the callback: |
| // * Acls are ordered by Role now. |
| // * READER is missing, the principals list was empty. |
| // * Principals are sorted by "users first, then groups". |
| expected := &api.PrefixMetadata{ |
| Prefix: "z", |
| Fingerprint: "ppDqWKGcl8Pu1hMiXQ1hac0vAH0", |
| UpdateTime: timestamppb.New(ts), |
| UpdateUser: "user:updater@example.com", |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"group:b"}, |
| }, |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"user:a@example.com", "group:a"}, |
| }, |
| }, |
| } |
| So(updated, ShouldResembleProto, expected) |
| |
| // Stored indeed. |
| md, err := impl.GetMetadata(ctx, "z") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, expected}) |
| }) |
| |
| Convey("UpdateMetadata call with failing callback", func() { |
| cbErr := errors.New("blah") |
| updated, err := impl.UpdateMetadata(ctx, "z", func(_ context.Context, md *api.PrefixMetadata) error { |
| md.UpdateUser = "user:must-be-ignored@example.com" |
| return cbErr |
| }) |
| So(err, ShouldEqual, cbErr) // exact same error object |
| So(updated, ShouldBeNil) |
| |
| // Still missing. |
| md, err := impl.GetMetadata(ctx, "z") |
| So(err, ShouldBeNil) |
| So(md, ShouldResembleProto, []*api.PrefixMetadata{rootMeta}) |
| }) |
| }) |
| } |
| |
| func TestVisitMetadata(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With datastore", t, func() { |
| ctx := memory.Use(context.Background()) |
| ts := time.Unix(1525136124, 0).UTC() |
| |
| impl := legacyStorageImpl{} |
| |
| add := func(role, pfx, group string) { |
| So(datastore.Put(ctx, &packageACL{ |
| ID: role + ":" + pfx, |
| Parent: rootKey(ctx), |
| ModifiedTS: ts, |
| Groups: []string{group}, |
| }), ShouldBeNil) |
| } |
| |
| type visited struct { |
| prefix string |
| md []string // pfx:role:principal, sorted |
| } |
| |
| visit := func(pfx string) (res []visited) { |
| err := impl.VisitMetadata(ctx, pfx, func(p string, md []*api.PrefixMetadata) (bool, error) { |
| extract := []string{} |
| for _, m := range md { |
| for _, acl := range m.Acls { |
| for _, p := range acl.Principals { |
| extract = append(extract, fmt.Sprintf("%s:%s:%s", m.Prefix, acl.Role, p)) |
| } |
| } |
| } |
| res = append(res, visited{p, extract}) |
| return true, nil |
| }) |
| So(err, ShouldBeNil) |
| return |
| } |
| |
| add("OWNER", "a", "o-a") |
| add("READER", "a", "r-a") |
| |
| add("OWNER", "a/b/c", "o-abc") |
| add("READER", "a/b/c", "r-abc") |
| add("WRITER", "a/b/c", "w-abc") |
| |
| add("READER", "a/b/c/d", "r-abcd") |
| |
| add("OWNER", "ab", "o-ab") |
| add("READER", "ab", "r-ab") |
| |
| Convey("Root listing", func() { |
| So(visit(""), ShouldResemble, []visited{ |
| { |
| "", []string{ |
| ":OWNER:group:administrators", |
| }, |
| }, |
| { |
| "a", []string{ |
| ":OWNER:group:administrators", |
| "a:OWNER:group:o-a", |
| "a:READER:group:r-a", |
| }, |
| }, |
| { |
| "a/b/c", []string{ |
| ":OWNER:group:administrators", |
| "a:OWNER:group:o-a", |
| "a:READER:group:r-a", |
| "a/b/c:OWNER:group:o-abc", |
| "a/b/c:WRITER:group:w-abc", |
| "a/b/c:READER:group:r-abc", |
| }, |
| }, |
| { |
| "a/b/c/d", []string{ |
| ":OWNER:group:administrators", |
| "a:OWNER:group:o-a", |
| "a:READER:group:r-a", |
| "a/b/c:OWNER:group:o-abc", |
| "a/b/c:WRITER:group:w-abc", |
| "a/b/c:READER:group:r-abc", |
| "a/b/c/d:READER:group:r-abcd", |
| }, |
| }, |
| { |
| "ab", []string{ |
| ":OWNER:group:administrators", |
| "ab:OWNER:group:o-ab", |
| "ab:READER:group:r-ab", |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Prefix listing", func() { |
| So(visit("a"), ShouldResemble, []visited{ |
| { |
| "a", []string{ |
| ":OWNER:group:administrators", |
| "a:OWNER:group:o-a", |
| "a:READER:group:r-a", |
| }, |
| }, |
| { |
| "a/b/c", []string{ |
| ":OWNER:group:administrators", |
| "a:OWNER:group:o-a", |
| "a:READER:group:r-a", |
| "a/b/c:OWNER:group:o-abc", |
| "a/b/c:WRITER:group:w-abc", |
| "a/b/c:READER:group:r-abc", |
| }, |
| }, |
| { |
| "a/b/c/d", []string{ |
| ":OWNER:group:administrators", |
| "a:OWNER:group:o-a", |
| "a:READER:group:r-a", |
| "a/b/c:OWNER:group:o-abc", |
| "a/b/c:WRITER:group:w-abc", |
| "a/b/c:READER:group:r-abc", |
| "a/b/c/d:READER:group:r-abcd", |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Missing prefix listing", func() { |
| So(visit("z/z/z"), ShouldResemble, []visited{ |
| { |
| "z/z/z", []string{":OWNER:group:administrators"}, |
| }, |
| }) |
| }) |
| |
| Convey("Callback return value is respected, stopping right away", func() { |
| seen := []string{} |
| err := impl.VisitMetadata(ctx, "a", func(p string, md []*api.PrefixMetadata) (bool, error) { |
| seen = append(seen, p) |
| return false, nil |
| }) |
| So(err, ShouldBeNil) |
| So(seen, ShouldResemble, []string{"a"}) |
| }) |
| |
| Convey("Callback return value is respected, stopping later", func() { |
| seen := []string{} |
| err := impl.VisitMetadata(ctx, "a", func(p string, md []*api.PrefixMetadata) (bool, error) { |
| seen = append(seen, p) |
| return p != "a/b/c", nil |
| }) |
| So(err, ShouldBeNil) |
| So(seen, ShouldResemble, []string{"a", "a/b/c"}) // no a/b/c/d |
| }) |
| }) |
| } |
| |
| func TestParseKey(t *testing.T) { |
| t.Parallel() |
| |
| cases := []struct { |
| id string |
| role string |
| prefix string |
| err string |
| }{ |
| {"OWNER:a/b/c", "OWNER", "a/b/c", ""}, |
| {"OWNER", "", "", "not <role>:<prefix> pair"}, |
| {"UNKNOWN:a/b/c", "", "", "unrecognized role"}, |
| {"OWNER:///", "", "", "invalid package prefix"}, |
| {"OWNER:", "", "", "invalid package prefix"}, |
| } |
| |
| for _, c := range cases { |
| Convey(fmt.Sprintf("works for %q", c.id), t, func() { |
| role, pfx, err := (&packageACL{ID: c.id}).parseKey() |
| So(role, ShouldEqual, c.role) |
| So(pfx, ShouldEqual, c.prefix) |
| if c.err == "" { |
| So(err, ShouldBeNil) |
| } else { |
| So(err, ShouldErrLike, c.err) |
| } |
| }) |
| } |
| } |
| |
| func TestListACLsByPrefix(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With datastore", t, func() { |
| ctx := memory.Use(context.Background()) |
| |
| add := func(role, pfx string) { |
| So(datastore.Put(ctx, &packageACL{ |
| ID: role + ":" + pfx, |
| Parent: rootKey(ctx), |
| Groups: []string{"blah"}, // to make sure bodies are fetched too |
| }), ShouldBeNil) |
| } |
| |
| list := func(role, pfx string) (out []string) { |
| acls, err := listACLsByPrefix(ctx, role, pfx) |
| So(err, ShouldBeNil) |
| for _, acl := range acls { |
| So(acl.Groups, ShouldResemble, []string{"blah"}) |
| out = append(out, acl.ID) |
| } |
| return |
| } |
| |
| add("OWNER", "a") |
| add("OWNER", "a/b/c") |
| add("OWNER", "ab") |
| add("READER", "a") |
| add("READER", "a/b/c") |
| add("READER", "a/b/c/d") |
| add("READER", "ab") |
| |
| Convey("Root listing", func() { |
| So(list("OWNER", ""), ShouldResemble, []string{ |
| "OWNER:a", "OWNER:a/b/c", "OWNER:ab", |
| }) |
| So(list("READER", ""), ShouldResemble, []string{ |
| "READER:a", "READER:a/b/c", "READER:a/b/c/d", "READER:ab", |
| }) |
| So(list("WRITER", ""), ShouldResemble, []string(nil)) |
| }) |
| |
| Convey("Non-root listing", func() { |
| So(list("OWNER", "a"), ShouldResemble, []string{"OWNER:a/b/c"}) |
| So(list("READER", "a"), ShouldResemble, []string{"READER:a/b/c", "READER:a/b/c/d"}) |
| So(list("WRITER", "a"), ShouldResemble, []string(nil)) |
| }) |
| |
| Convey("Non-existing prefix listing", func() { |
| So(list("OWNER", "z"), ShouldResemble, []string(nil)) |
| }) |
| }) |
| } |
| |
| func TestMetadataGraph(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With metadataGraph", t, func() { |
| ctx := memory.Use(context.Background()) |
| ts := time.Unix(1525136124, 0).UTC() |
| |
| gr := metadataGraph{} |
| gr.init(&api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"group:root"}, |
| }, |
| }, |
| }) |
| |
| insert := func(role, prefix, group string) { |
| gr.insert(ctx, []*packageACL{ |
| { |
| ID: role + ":" + prefix, |
| Parent: rootKey(ctx), |
| Groups: []string{group}, |
| ModifiedTS: ts, // to mark as non-empty |
| }, |
| }) |
| } |
| |
| type visited struct { |
| prefix string |
| md []string // pfx:role:principal, sorted |
| } |
| |
| freezeAndVisit := func(node string) (v []visited) { |
| n := gr.node(node) |
| gr.freeze(ctx) |
| |
| err := n.traverse(nil, func(n *metadataNode, md []*api.PrefixMetadata) (bool, error) { |
| extract := []string{} |
| for _, m := range md { |
| for _, acl := range m.Acls { |
| for _, p := range acl.Principals { |
| extract = append(extract, fmt.Sprintf("%s:%s:%s", m.Prefix, acl.Role, p)) |
| } |
| } |
| } |
| v = append(v, visited{n.prefix, extract}) |
| return true, nil |
| }) |
| So(err, ShouldBeNil) |
| return |
| } |
| |
| insert("OWNER", "a/b/c/d", "owner-abc") |
| insert("OWNER", "a", "owner-a") |
| insert("OWNER", "b", "owner-b") |
| insert("READER", "a", "reader-a") |
| insert("READER", "a/b", "reader-ab") |
| insert("READER", "a/bc", "reader-abc") |
| insert("BOGUS", "a/b", "bogus-ab") |
| |
| Convey("Traverse from the root", func() { |
| So(freezeAndVisit(""), ShouldResemble, []visited{ |
| {"", []string{":OWNER:group:root"}}, |
| { |
| "a", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| }, |
| }, |
| { |
| "a/b", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| }, |
| }, |
| { |
| "a/b/c", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| }, |
| }, |
| { |
| "a/b/c/d", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| "a/b/c/d:OWNER:group:owner-abc", |
| }, |
| }, |
| { |
| "a/bc", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/bc:READER:group:reader-abc", |
| }, |
| }, |
| { |
| "b", []string{ |
| ":OWNER:group:root", |
| "b:OWNER:group:owner-b", |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Traverse from some prefix", func() { |
| So(freezeAndVisit("a/b"), ShouldResemble, []visited{ |
| { |
| "a/b", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| }, |
| }, |
| { |
| "a/b/c", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| }, |
| }, |
| { |
| "a/b/c/d", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| "a/b/c/d:OWNER:group:owner-abc", |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Traverse from some deep prefix", func() { |
| So(freezeAndVisit("a/b/c/d/e"), ShouldResemble, []visited{ |
| { |
| "a/b/c/d/e", []string{ |
| ":OWNER:group:root", |
| "a:OWNER:group:owner-a", |
| "a:READER:group:reader-a", |
| "a/b:READER:group:reader-ab", |
| "a/b/c/d:OWNER:group:owner-abc", |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Traverse from some non-existing prefix", func() { |
| So(freezeAndVisit("z/z/z"), ShouldResemble, []visited{ |
| { |
| "z/z/z", []string{":OWNER:group:root"}, |
| }, |
| }) |
| }) |
| }) |
| } |