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 }