| // 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 repo |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| "net/url" |
| "strings" |
| "testing" |
| "time" |
| |
| "github.com/julienschmidt/httprouter" |
| |
| statuspb "google.golang.org/genproto/googleapis/rpc/status" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| "google.golang.org/protobuf/proto" |
| "google.golang.org/protobuf/types/known/timestamppb" |
| |
| "go.chromium.org/luci/auth/identity" |
| "go.chromium.org/luci/common/retry/transient" |
| "go.chromium.org/luci/gae/service/datastore" |
| "go.chromium.org/luci/server/auth" |
| "go.chromium.org/luci/server/auth/authtest" |
| "go.chromium.org/luci/server/router" |
| "go.chromium.org/luci/server/tq" |
| |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| "go.chromium.org/luci/cipd/appengine/impl/gs" |
| "go.chromium.org/luci/cipd/appengine/impl/model" |
| "go.chromium.org/luci/cipd/appengine/impl/repo/processing" |
| "go.chromium.org/luci/cipd/appengine/impl/repo/tasks" |
| "go.chromium.org/luci/cipd/appengine/impl/testutil" |
| "go.chromium.org/luci/cipd/common" |
| |
| . "github.com/smartystreets/goconvey/convey" |
| . "go.chromium.org/luci/common/testing/assertions" |
| |
| // Using transactional datastore TQ tasks. |
| _ "go.chromium.org/luci/server/tq/txn/datastore" |
| ) |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Prefix metadata RPC methods + related helpers including ACL checks. |
| |
| func TestMetadataFetching(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| _, _, as := testutil.TestingContext( |
| authtest.MockMembership("user:prefixes-viewer@example.com", PrefixesViewers), |
| ) |
| |
| meta := testutil.MetadataStore{} |
| |
| // ACL. |
| rootMeta := meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:admin@example.com"}, |
| }, |
| }, |
| }) |
| topMeta := meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:top-owner@example.com"}, |
| }, |
| }, |
| }) |
| |
| // The metadata to be fetched. |
| leafMeta := meta.Populate("a/b/c/d", &api.PrefixMetadata{ |
| UpdateUser: "user:someone@example.com", |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| callGet := func(prefix string, user identity.Identity) (*api.PrefixMetadata, error) { |
| return impl.GetPrefixMetadata(as(user.Email()), &api.PrefixRequest{Prefix: prefix}) |
| } |
| |
| callGetInherited := func(prefix string, user identity.Identity) ([]*api.PrefixMetadata, error) { |
| resp, err := impl.GetInheritedPrefixMetadata(as(user.Email()), &api.PrefixRequest{Prefix: prefix}) |
| if err != nil { |
| return nil, err |
| } |
| return resp.PerPrefixMetadata, nil |
| } |
| |
| Convey("GetPrefixMetadata happy path", func() { |
| resp, err := callGet("a/b/c/d", "user:top-owner@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, leafMeta) |
| }) |
| |
| Convey("GetPrefixMetadata happy path via global group", func() { |
| resp, err := callGet("a/b/c/d", "user:prefixes-viewer@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, leafMeta) |
| }) |
| |
| Convey("GetInheritedPrefixMetadata happy path", func() { |
| resp, err := callGetInherited("a/b/c/d", "user:top-owner@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, topMeta, leafMeta}) |
| }) |
| |
| Convey("GetInheritedPrefixMetadata happy path via global group", func() { |
| resp, err := callGetInherited("a/b/c/d", "user:prefixes-viewer@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, topMeta, leafMeta}) |
| }) |
| |
| Convey("GetPrefixMetadata bad prefix", func() { |
| resp, err := callGet("a//", "user:top-owner@example.com") |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(resp, ShouldBeNil) |
| }) |
| |
| Convey("GetInheritedPrefixMetadata bad prefix", func() { |
| resp, err := callGetInherited("a//", "user:top-owner@example.com") |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(resp, ShouldBeNil) |
| }) |
| |
| Convey("GetPrefixMetadata no metadata, caller has access", func() { |
| resp, err := callGet("a/b", "user:top-owner@example.com") |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(resp, ShouldBeNil) |
| }) |
| |
| Convey("GetInheritedPrefixMetadata no metadata, caller has access", func() { |
| resp, err := callGetInherited("a/b", "user:top-owner@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, []*api.PrefixMetadata{rootMeta, topMeta}) |
| }) |
| |
| Convey("GetPrefixMetadata no metadata, caller has no access", func() { |
| resp, err := callGet("a/b", "user:someone-else@example.com") |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(resp, ShouldBeNil) |
| // Existing metadata that the caller has no access to produces same error, |
| // so unauthorized callers can't easily distinguish between the two. |
| resp, err = callGet("a/b/c/d", "user:someone-else@example.com") |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(resp, ShouldBeNil) |
| // Same for completely unknown prefix. |
| resp, err = callGet("zzz", "user:someone-else@example.com") |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(resp, ShouldBeNil) |
| }) |
| |
| Convey("GetInheritedPrefixMetadata no metadata, caller has no access", func() { |
| resp, err := callGetInherited("a/b", "user:someone-else@example.com") |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(resp, ShouldBeNil) |
| // Existing metadata that the caller has no access to produces same error, |
| // so unauthorized callers can't easily distinguish between the two. |
| resp, err = callGetInherited("a/b/c/d", "user:someone-else@example.com") |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(resp, ShouldBeNil) |
| // Same for completely unknown prefix. |
| resp, err = callGetInherited("zzz", "user:someone-else@example.com") |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(resp, ShouldBeNil) |
| }) |
| }) |
| } |
| |
| func TestMetadataUpdating(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, tc, as := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| |
| // ACL. |
| meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:admin@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:top-owner@example.com"}, |
| }, |
| }, |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| callUpdate := func(user identity.Identity, m *api.PrefixMetadata) (md *api.PrefixMetadata, err error) { |
| err = datastore.RunInTransaction(as(user.Email()), func(ctx context.Context) (err error) { |
| md, err = impl.UpdatePrefixMetadata(ctx, m) |
| return |
| }, nil) |
| return |
| } |
| |
| Convey("Happy path", func() { |
| // Create new metadata entry. |
| meta, err := callUpdate("user:top-owner@example.com", &api.PrefixMetadata{ |
| Prefix: "a/b/", |
| UpdateTime: timestamppb.New(time.Unix(10000, 0)), // should be overwritten |
| UpdateUser: "user:zzz@example.com", // should be overwritten |
| Acls: []*api.PrefixMetadata_ACL{ |
| {Role: api.Role_READER, Principals: []string{"user:reader@example.com"}}, |
| }, |
| }) |
| So(err, ShouldBeNil) |
| |
| expected := &api.PrefixMetadata{ |
| Prefix: "a/b", |
| Fingerprint: "WZllwc6m8f9C_rfwnspaPIiyPD0", |
| UpdateTime: timestamppb.New(testutil.TestTime), |
| UpdateUser: "user:top-owner@example.com", |
| Acls: []*api.PrefixMetadata_ACL{ |
| {Role: api.Role_READER, Principals: []string{"user:reader@example.com"}}, |
| }, |
| } |
| So(meta, ShouldResembleProto, expected) |
| |
| // Update it a bit later. |
| tc.Add(time.Hour) |
| updated := proto.Clone(expected).(*api.PrefixMetadata) |
| updated.Acls = nil |
| meta, err = callUpdate("user:top-owner@example.com", updated) |
| So(err, ShouldBeNil) |
| So(meta, ShouldResembleProto, &api.PrefixMetadata{ |
| Prefix: "a/b", |
| Fingerprint: "oQ2uuVbjV79prXxl4jyJkOpff90", |
| UpdateTime: timestamppb.New(testutil.TestTime.Add(time.Hour)), |
| UpdateUser: "user:top-owner@example.com", |
| }) |
| |
| // Have these in the event log as well. |
| datastore.GetTestable(ctx).CatchupIndexes() |
| ev, err := model.QueryEvents(ctx, model.NewEventsQuery()) |
| So(err, ShouldBeNil) |
| So(ev, ShouldResembleProto, []*api.Event{ |
| { |
| Kind: api.EventKind_PREFIX_ACL_CHANGED, |
| Package: "a/b", |
| Who: "user:top-owner@example.com", |
| When: timestamppb.New(testutil.TestTime.Add(time.Hour)), |
| RevokedRole: []*api.PrefixMetadata_ACL{ |
| {Role: api.Role_READER, Principals: []string{"user:reader@example.com"}}, |
| }, |
| }, |
| { |
| Kind: api.EventKind_PREFIX_ACL_CHANGED, |
| Package: "a/b", |
| Who: "user:top-owner@example.com", |
| When: timestamppb.New(testutil.TestTime), |
| GrantedRole: []*api.PrefixMetadata_ACL{ |
| {Role: api.Role_READER, Principals: []string{"user:reader@example.com"}}, |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Validation works", func() { |
| meta, err := callUpdate("user:top-owner@example.com", &api.PrefixMetadata{ |
| Prefix: "a/b//", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(meta, ShouldBeNil) |
| |
| meta, err = callUpdate("user:top-owner@example.com", &api.PrefixMetadata{ |
| Prefix: "a/b", |
| Acls: []*api.PrefixMetadata_ACL{ |
| {Role: api.Role_READER, Principals: []string{"huh?"}}, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(meta, ShouldBeNil) |
| }) |
| |
| Convey("ACLs work", func() { |
| meta, err := callUpdate("user:unknown@example.com", &api.PrefixMetadata{ |
| Prefix: "a/b", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(meta, ShouldBeNil) |
| |
| // Same as completely unknown prefix. |
| meta, err = callUpdate("user:unknown@example.com", &api.PrefixMetadata{ |
| Prefix: "zzz", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(meta, ShouldBeNil) |
| }) |
| |
| Convey("Deleted concurrently", func() { |
| m := meta.Populate("a/b", &api.PrefixMetadata{ |
| UpdateUser: "user:someone@example.com", |
| }) |
| meta.Purge("a/b") |
| |
| // If the caller is a prefix owner, they see NotFound. |
| meta, err := callUpdate("user:top-owner@example.com", m) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(meta, ShouldBeNil) |
| |
| // Other callers just see regular PermissionDenined. |
| meta, err = callUpdate("user:unknown@example.com", m) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(meta, ShouldBeNil) |
| }) |
| |
| Convey("Creating existing", func() { |
| m := meta.Populate("a/b", &api.PrefixMetadata{ |
| UpdateUser: "user:someone@example.com", |
| }) |
| |
| m.Fingerprint = "" // indicates the caller is expecting to create a new one |
| meta, err := callUpdate("user:top-owner@example.com", m) |
| So(status.Code(err), ShouldEqual, codes.AlreadyExists) |
| So(meta, ShouldBeNil) |
| }) |
| |
| Convey("Changed midway", func() { |
| m := meta.Populate("a/b", &api.PrefixMetadata{ |
| UpdateUser: "user:someone@example.com", |
| }) |
| |
| // Someone comes and updates it. |
| updated, err := callUpdate("user:top-owner@example.com", m) |
| So(err, ShouldBeNil) |
| So(updated.Fingerprint, ShouldNotEqual, m.Fingerprint) |
| |
| // Trying to do it again fails, 'm' is stale now. |
| _, err = callUpdate("user:top-owner@example.com", m) |
| So(status.Code(err), ShouldEqual, codes.FailedPrecondition) |
| }) |
| }) |
| } |
| |
| func TestGetRolesInPrefixOnBehalfOf(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| rootCtx, _, _ := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| |
| meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:admin@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"user:writer@example.com"}, |
| }, |
| }, |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| call := func(prefix string, user identity.Identity, callerIsPrefixViewer bool) (*api.RolesInPrefixResponse, error) { |
| caller := identity.Identity("user:roles-caller@example.com") |
| mocks := []authtest.MockedDatum{} |
| if callerIsPrefixViewer { |
| mocks = append(mocks, authtest.MockMembership(caller, PrefixesViewers)) |
| } |
| return impl.GetRolesInPrefixOnBehalfOf(auth.WithState(rootCtx, &authtest.FakeState{ |
| Identity: caller, |
| FakeDB: authtest.NewFakeDB(mocks...), |
| }), &api.PrefixRequestOnBehalfOf{Identity: string(user), PrefixRequest: &api.PrefixRequest{Prefix: prefix}}) |
| } |
| |
| Convey("Happy path", func() { |
| resp, err := call("a/b/c/d", "user:writer@example.com", true) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RolesInPrefixResponse{ |
| Roles: []*api.RolesInPrefixResponse_RoleInPrefix{ |
| {Role: api.Role_READER}, |
| {Role: api.Role_WRITER}, |
| }, |
| }) |
| }) |
| |
| Convey("Anonymous", func() { |
| resp, err := call("a/b/c/d", "anonymous:anonymous", true) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RolesInPrefixResponse{}) |
| }) |
| |
| Convey("Admin", func() { |
| resp, err := call("a/b/c/d", "user:admin@example.com", true) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RolesInPrefixResponse{ |
| Roles: []*api.RolesInPrefixResponse_RoleInPrefix{ |
| {Role: api.Role_READER}, |
| {Role: api.Role_WRITER}, |
| {Role: api.Role_OWNER}, |
| }, |
| }) |
| }) |
| |
| Convey("Bad prefix", func() { |
| _, err := call("///", "user:writer@example.com", true) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'prefix'") |
| }) |
| |
| Convey("Not prefix viewer", func() { |
| _, err := call("a/b/c/d", "user:writer@example.com", false) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| }) |
| |
| Convey("Disallowed identity types", func() { |
| for _, kind := range []identity.Kind{identity.Bot, identity.Project, identity.Service} { |
| ident, err := identity.MakeIdentity(string(kind) + ":somevalue") |
| So(err, ShouldBeNil) |
| |
| _, err = call("a/b/c/d", ident, true) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| } |
| }) |
| |
| }) |
| } |
| |
| func TestGetRolesInPrefix(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| rootCtx, _, _ := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| |
| meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:admin@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"user:writer@example.com"}, |
| }, |
| }, |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| call := func(prefix string, user identity.Identity) (*api.RolesInPrefixResponse, error) { |
| return impl.GetRolesInPrefix(auth.WithState(rootCtx, &authtest.FakeState{ |
| Identity: user, |
| FakeDB: authtest.NewFakeDB(), |
| }), &api.PrefixRequest{Prefix: prefix}) |
| } |
| |
| Convey("Happy path", func() { |
| resp, err := call("a/b/c/d", "user:writer@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RolesInPrefixResponse{ |
| Roles: []*api.RolesInPrefixResponse_RoleInPrefix{ |
| {Role: api.Role_READER}, |
| {Role: api.Role_WRITER}, |
| }, |
| }) |
| }) |
| |
| Convey("Anonymous", func() { |
| resp, err := call("a/b/c/d", "anonymous:anonymous") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RolesInPrefixResponse{}) |
| }) |
| |
| Convey("Admin", func() { |
| resp, err := call("a/b/c/d", "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RolesInPrefixResponse{ |
| Roles: []*api.RolesInPrefixResponse_RoleInPrefix{ |
| {Role: api.Role_READER}, |
| {Role: api.Role_WRITER}, |
| {Role: api.Role_OWNER}, |
| }, |
| }) |
| }) |
| |
| Convey("Bad prefix", func() { |
| _, err := call("///", "user:writer@example.com") |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'prefix'") |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Prefix listing. |
| |
| func TestListPrefix(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| |
| meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:admin@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("1/a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("6", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("7", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| call := func(prefix string, recursive, hidden bool, user identity.Identity) (*api.ListPrefixResponse, error) { |
| return impl.ListPrefix(as(user.Email()), &api.ListPrefixRequest{ |
| Prefix: prefix, |
| Recursive: recursive, |
| IncludeHidden: hidden, |
| }) |
| } |
| |
| const hidden = true |
| const visible = false |
| mk := func(name string, hidden bool) { |
| So(datastore.Put(ctx, &model.Package{ |
| Name: name, |
| Hidden: hidden, |
| }), ShouldBeNil) |
| } |
| |
| // Note: "1" is both a package and a prefix, this is allowed. |
| mk("1", visible) |
| mk("1/a", visible) // note: readable to reader@... |
| mk("1/b", visible) |
| mk("1/c", hidden) |
| mk("1/d/a", hidden) |
| mk("1/a/b", visible) // note: readable to reader@... |
| mk("1/a/b/c", visible) // note: readable to reader@... |
| mk("1/a/c", hidden) // note: readable to reader@... |
| mk("2/a/b/c", visible) |
| mk("3", visible) |
| mk("4", hidden) |
| mk("5/a/b", hidden) |
| mk("6", hidden) // note: readable to reader@... |
| mk("6/a/b", visible) // note: readable to reader@... |
| mk("7/a", hidden) // note: readable to reader@... |
| datastore.GetTestable(ctx).CatchupIndexes() |
| |
| // Note about the test cases names below: |
| // * "Full" means there are no ACL restriction. |
| // * "Restricted" means some results are filtered out by ACLs. |
| // * "Root" means listing root of the repo. |
| // * "Non-root" means listing some prefix. |
| // * "Recursive" is obvious. |
| // * "Non-recursive" is also obvious. |
| // * "Including hidden" means results includes hidden packages. |
| // * "Visible only" means results includes only non-hidden packages. |
| // |
| // This 4 test dimensions => 16 test cases. |
| |
| Convey("Full listing", func() { |
| Convey("Root recursive (including hidden)", func() { |
| resp, err := call("", true, true, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1", "1/a", "1/a/b", "1/a/b/c", "1/a/c", "1/b", "1/c", "1/d/a", |
| "2/a/b/c", "3", "4", "5/a/b", "6", "6/a/b", "7/a", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{ |
| "1", "1/a", "1/a/b", "1/d", "2", "2/a", "2/a/b", "5", "5/a", |
| "6", "6/a", "7", |
| }) |
| }) |
| |
| Convey("Root recursive (visible only)", func() { |
| resp, err := call("", true, false, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1", "1/a", "1/a/b", "1/a/b/c", "1/b", "2/a/b/c", "3", "6/a/b", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{ |
| "1", "1/a", "1/a/b", "2", "2/a", "2/a/b", "6", "6/a", |
| }) |
| }) |
| |
| Convey("Root non-recursive (including hidden)", func() { |
| resp, err := call("", false, true, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{"1", "3", "4", "6"}) |
| So(resp.Prefixes, ShouldResemble, []string{"1", "2", "5", "6", "7"}) |
| }) |
| |
| Convey("Root non-recursive (visible only)", func() { |
| resp, err := call("", false, false, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{"1", "3"}) |
| So(resp.Prefixes, ShouldResemble, []string{"1", "2", "6"}) |
| }) |
| |
| Convey("Non-root recursive (including hidden)", func() { |
| resp, err := call("1", true, true, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1/a", "1/a/b", "1/a/b/c", "1/a/c", "1/b", "1/c", "1/d/a", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{"1/a", "1/a/b", "1/d"}) |
| }) |
| |
| Convey("Non-root recursive (visible only)", func() { |
| resp, err := call("1", true, false, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1/a", "1/a/b", "1/a/b/c", "1/b", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{"1/a", "1/a/b"}) |
| }) |
| }) |
| |
| Convey("Restricted listing", func() { |
| Convey("Root recursive (including hidden)", func() { |
| resp, err := call("", true, true, "user:reader@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1/a", "1/a/b", "1/a/b/c", "1/a/c", "6", "6/a/b", "7/a", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{ |
| "1", "1/a", "1/a/b", "6", "6/a", "7", |
| }) |
| }) |
| |
| Convey("Root recursive (visible only)", func() { |
| resp, err := call("", true, false, "user:reader@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1/a", "1/a/b", "1/a/b/c", "6/a/b", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{ |
| "1", "1/a", "1/a/b", "6", "6/a", |
| }) |
| }) |
| |
| Convey("Root non-recursive (including hidden)", func() { |
| resp, err := call("", false, true, "user:reader@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{"6"}) |
| So(resp.Prefixes, ShouldResemble, []string{"1", "6", "7"}) |
| }) |
| |
| Convey("Root non-recursive (visible only)", func() { |
| resp, err := call("", false, false, "user:reader@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string(nil)) |
| So(resp.Prefixes, ShouldResemble, []string{"1", "6"}) |
| }) |
| |
| Convey("Non-root recursive (including hidden)", func() { |
| resp, err := call("1", true, true, "user:reader@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1/a", "1/a/b", "1/a/b/c", "1/a/c", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{"1/a", "1/a/b"}) |
| }) |
| |
| Convey("Non-root recursive (visible only)", func() { |
| resp, err := call("1", true, false, "user:reader@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldResemble, []string{ |
| "1/a", "1/a/b", "1/a/b/c", |
| }) |
| So(resp.Prefixes, ShouldResemble, []string{"1/a", "1/a/b"}) |
| }) |
| }) |
| |
| Convey("The package is not listed when listing its name directly", func() { |
| resp, err := call("3", true, true, "user:admin@example.com") |
| So(err, ShouldBeNil) |
| So(resp.Packages, ShouldHaveLength, 0) |
| So(resp.Prefixes, ShouldHaveLength, 0) |
| }) |
| |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Hide/unhide package. |
| |
| func TestHideUnhidePackage(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("owner@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:owner@example.com"}, |
| }, |
| }, |
| }) |
| |
| So(datastore.Put(ctx, &model.Package{Name: "a/b"}), ShouldBeNil) |
| |
| fetch := func(pkg string) *model.Package { |
| p := &model.Package{Name: pkg} |
| So(datastore.Get(ctx, p), ShouldBeNil) |
| return p |
| } |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("Hides and unhides", func() { |
| _, err := impl.HidePackage(ctx, &api.PackageRequest{Package: "a/b"}) |
| So(err, ShouldBeNil) |
| So(fetch("a/b").Hidden, ShouldBeTrue) |
| |
| // Noop is fine. |
| _, err = impl.HidePackage(ctx, &api.PackageRequest{Package: "a/b"}) |
| So(err, ShouldBeNil) |
| So(fetch("a/b").Hidden, ShouldBeTrue) |
| |
| _, err = impl.UnhidePackage(ctx, &api.PackageRequest{Package: "a/b"}) |
| So(err, ShouldBeNil) |
| So(fetch("a/b").Hidden, ShouldBeFalse) |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := impl.HidePackage(ctx, &api.PackageRequest{Package: "///"}) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid package name") |
| }) |
| |
| Convey("No access", func() { |
| _, err := impl.HidePackage(ctx, &api.PackageRequest{Package: "zzz"}) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "not allowed to see it") |
| }) |
| |
| Convey("Missing package", func() { |
| _, err := impl.HidePackage(ctx, &api.PackageRequest{Package: "a/b/c"}) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Package deletion. |
| |
| func TestDeletePackage(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:root@example.com"}, |
| }, |
| }, |
| }) |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:non-root-owner@example.com"}, |
| }, |
| }, |
| }) |
| |
| So(datastore.Put(ctx, &model.Package{Name: "a/b"}), ShouldBeNil) |
| So(model.CheckPackageExists(ctx, "a/b"), ShouldBeNil) |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("Works", func() { |
| _, err := impl.DeletePackage(as("root@example.com"), &api.PackageRequest{ |
| Package: "a/b", |
| }) |
| So(err, ShouldBeNil) |
| |
| // Gone now. |
| So(model.CheckPackageExists(ctx, "a/b"), ShouldNotBeNil) |
| |
| _, err = impl.DeletePackage(as("root@example.com"), &api.PackageRequest{ |
| Package: "a/b", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| |
| Convey("Only reader or above can see", func() { |
| _, err := impl.DeletePackage(as("someone@example.com"), &api.PackageRequest{ |
| Package: "a/b", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| |
| Convey("Only root owner can delete", func() { |
| _, err := impl.DeletePackage(as("non-root-owner@example.com"), &api.PackageRequest{ |
| Package: "a/b", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "allowed only to service administrators") |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := impl.DeletePackage(ctx, &api.PackageRequest{Package: "///"}) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid package name") |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Package instance registration and post-registration processing. |
| |
| func TestRegisterInstance(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("owner@example.com") |
| |
| cas := testutil.MockCAS{} |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:owner@example.com"}, |
| }, |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| dispatcher := &tq.Dispatcher{} |
| ctx, sched := tq.TestingContext(ctx, dispatcher) |
| |
| impl := repoImpl{ |
| tq: dispatcher, |
| meta: &meta, |
| cas: &cas, |
| } |
| impl.registerTasks() |
| |
| digest := strings.Repeat("a", 40) |
| inst := &api.Instance{ |
| Package: "a/b", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| } |
| |
| Convey("Happy path", func() { |
| impl.registerProcessor(&mockedProcessor{ |
| ProcID: "proc_id_1", |
| AppliesTo: inst.Package, |
| }) |
| impl.registerProcessor(&mockedProcessor{ |
| ProcID: "proc_id_2", |
| AppliesTo: "something else", |
| }) |
| |
| uploadOp := api.UploadOperation{ |
| OperationId: "op_id", |
| UploadUrl: "http://fake.example.com", |
| Status: api.UploadStatus_UPLOADING, |
| } |
| |
| // Mock "successfully started upload op". |
| cas.BeginUploadImpl = func(_ context.Context, req *api.BeginUploadRequest) (*api.UploadOperation, error) { |
| So(req, ShouldResembleProto, &api.BeginUploadRequest{ |
| Object: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| return &uploadOp, nil |
| } |
| |
| // The instance is not uploaded yet => asks to upload. |
| resp, err := impl.RegisterInstance(ctx, inst) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RegisterInstanceResponse{ |
| Status: api.RegistrationStatus_NOT_UPLOADED, |
| UploadOp: &uploadOp, |
| }) |
| |
| // Mock "already have it in the storage" response. |
| cas.BeginUploadImpl = func(context.Context, *api.BeginUploadRequest) (*api.UploadOperation, error) { |
| return nil, status.Errorf(codes.AlreadyExists, "already uploaded") |
| } |
| |
| // The instance is already uploaded => registers it in the datastore. |
| fullInstProto := &api.Instance{ |
| Package: inst.Package, |
| Instance: inst.Instance, |
| RegisteredBy: "user:owner@example.com", |
| RegisteredTs: timestamppb.New(testutil.TestTime), |
| } |
| resp, err = impl.RegisterInstance(ctx, inst) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RegisterInstanceResponse{ |
| Status: api.RegistrationStatus_REGISTERED, |
| Instance: fullInstProto, |
| }) |
| |
| // Launched post-processors. |
| ent := (&model.Instance{}).FromProto(ctx, inst) |
| So(datastore.Get(ctx, ent), ShouldBeNil) |
| So(ent.ProcessorsPending, ShouldResemble, []string{"proc_id_1"}) |
| tqt := sched.Tasks() |
| So(tqt, ShouldHaveLength, 1) |
| So(tqt[0].Payload, ShouldResembleProto, &tasks.RunProcessors{ |
| Instance: fullInstProto, |
| }) |
| }) |
| |
| Convey("Already registered", func() { |
| instance := (&model.Instance{ |
| RegisteredBy: "user:someone@example.com", |
| }).FromProto(ctx, inst) |
| _, _, err := model.RegisterInstance(ctx, instance, nil) |
| So(err, ShouldBeNil) |
| |
| resp, err := impl.RegisterInstance(ctx, inst) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.RegisterInstanceResponse{ |
| Status: api.RegistrationStatus_ALREADY_REGISTERED, |
| Instance: &api.Instance{ |
| Package: inst.Package, |
| Instance: inst.Instance, |
| RegisteredBy: "user:someone@example.com", |
| }, |
| }) |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := impl.RegisterInstance(ctx, &api.Instance{ |
| Package: "//a", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| |
| Convey("Bad instance ID", func() { |
| _, err := impl.RegisterInstance(ctx, &api.Instance{ |
| Package: "a/b", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: "abc", |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| |
| Convey("No reader access", func() { |
| _, err := impl.RegisterInstance(ctx, &api.Instance{ |
| Package: "some/other/root", |
| Instance: inst.Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, `prefix "some/other/root" doesn't exist or "user:owner@example.com" is not allowed to see it`) |
| }) |
| |
| Convey("No owner access", func() { |
| _, err := impl.RegisterInstance(as("reader@example.com"), inst) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, `"user:reader@example.com" has no required WRITER role in prefix "a/b"`) |
| }) |
| }) |
| } |
| |
| func TestProcessors(t *testing.T) { |
| t.Parallel() |
| |
| testZip := testutil.MakeZip(map[string]string{ |
| "file1": strings.Repeat("hello", 50), |
| "file2": "blah", |
| }) |
| |
| Convey("With mocks", t, func() { |
| ctx, _, _ := testutil.TestingContext() |
| |
| cas := testutil.MockCAS{} |
| impl := repoImpl{cas: &cas} |
| |
| inst := &api.Instance{ |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: strings.Repeat("a", 40), |
| }, |
| } |
| |
| storeInstance := func(pending []string) { |
| i := (&model.Instance{ProcessorsPending: pending}).FromProto(ctx, inst) |
| So(datastore.Put(ctx, i), ShouldBeNil) |
| } |
| |
| fetchInstance := func() *model.Instance { |
| i := (&model.Instance{}).FromProto(ctx, inst) |
| So(datastore.Get(ctx, i), ShouldBeNil) |
| return i |
| } |
| |
| fetchProcRes := func(id string) *model.ProcessingResult { |
| i := (&model.Instance{}).FromProto(ctx, inst) |
| p := &model.ProcessingResult{ |
| ProcID: id, |
| Instance: datastore.KeyForObj(ctx, i), |
| } |
| So(datastore.Get(ctx, p), ShouldBeNil) |
| return p |
| } |
| |
| goodResult := map[string]string{"result": "OK"} |
| |
| // Note: assumes Result is a map[string]string. |
| fetchProcSuccess := func(id string) string { |
| res := fetchProcRes(id) |
| So(res, ShouldNotBeNil) |
| So(res.Success, ShouldBeTrue) |
| var r map[string]string |
| So(res.ReadResult(&r), ShouldBeNil) |
| return r["result"] |
| } |
| |
| fetchProcFail := func(id string) string { |
| res := fetchProcRes(id) |
| So(res, ShouldNotBeNil) |
| So(res.Success, ShouldBeFalse) |
| return res.Error |
| } |
| |
| Convey("Noop updateProcessors", func() { |
| storeInstance([]string{"a", "b"}) |
| So(impl.updateProcessors(ctx, inst, map[string]processing.Result{ |
| "some-another": {Err: fmt.Errorf("fail")}, |
| }), ShouldBeNil) |
| So(fetchInstance().ProcessorsPending, ShouldResemble, []string{"a", "b"}) |
| }) |
| |
| Convey("Updates processors successfully", func() { |
| storeInstance([]string{"ok", "fail", "pending"}) |
| |
| So(impl.updateProcessors(ctx, inst, map[string]processing.Result{ |
| "ok": {Result: goodResult}, |
| "fail": {Err: fmt.Errorf("failed")}, |
| }), ShouldBeNil) |
| |
| // Updated the Instance entity. |
| inst := fetchInstance() |
| So(inst.ProcessorsPending, ShouldResemble, []string{"pending"}) |
| So(inst.ProcessorsSuccess, ShouldResemble, []string{"ok"}) |
| So(inst.ProcessorsFailure, ShouldResemble, []string{"fail"}) |
| |
| // Created ProcessingResult entities. |
| So(fetchProcSuccess("ok"), ShouldEqual, "OK") |
| So(fetchProcFail("fail"), ShouldEqual, "failed") |
| }) |
| |
| Convey("Missing entity in updateProcessors", func() { |
| err := impl.updateProcessors(ctx, inst, map[string]processing.Result{ |
| "proc": {Err: fmt.Errorf("fail")}, |
| }) |
| So(err, ShouldErrLike, "the entity is unexpectedly gone") |
| }) |
| |
| Convey("runProcessorsTask happy path", func() { |
| // Setup two pending processors that read 'file2'. |
| runCB := func(i *model.Instance, r *processing.PackageReader) (processing.Result, error) { |
| So(i.Proto(), ShouldResembleProto, inst) |
| |
| rd, _, err := r.Open("file2") |
| So(err, ShouldBeNil) |
| defer rd.Close() |
| blob, err := io.ReadAll(rd) |
| So(err, ShouldBeNil) |
| So(string(blob), ShouldEqual, "blah") |
| |
| return processing.Result{Result: goodResult}, nil |
| } |
| |
| impl.registerProcessor(&mockedProcessor{ProcID: "proc1", RunCB: runCB}) |
| impl.registerProcessor(&mockedProcessor{ProcID: "proc2", RunCB: runCB}) |
| storeInstance([]string{"proc1", "proc2"}) |
| |
| // Setup the package. |
| cas.GetReaderImpl = func(_ context.Context, ref *api.ObjectRef) (gs.Reader, error) { |
| So(inst.Instance, ShouldResembleProto, ref) |
| return testutil.NewMockGSReader(testZip), nil |
| } |
| |
| // Run the processor. |
| err := impl.runProcessorsTask(ctx, &tasks.RunProcessors{Instance: inst}) |
| So(err, ShouldBeNil) |
| |
| // Both succeeded. |
| inst := fetchInstance() |
| So(inst.ProcessorsPending, ShouldHaveLength, 0) |
| So(inst.ProcessorsSuccess, ShouldResemble, []string{"proc1", "proc2"}) |
| |
| // And have the result. |
| So(fetchProcSuccess("proc1"), ShouldEqual, "OK") |
| So(fetchProcSuccess("proc2"), ShouldEqual, "OK") |
| }) |
| |
| Convey("runProcessorsTask no entity", func() { |
| err := impl.runProcessorsTask(ctx, &tasks.RunProcessors{Instance: inst}) |
| So(err, ShouldErrLike, "unexpectedly gone from the datastore") |
| }) |
| |
| Convey("runProcessorsTask no processor", func() { |
| storeInstance([]string{"proc"}) |
| |
| err := impl.runProcessorsTask(ctx, &tasks.RunProcessors{Instance: inst}) |
| So(err, ShouldBeNil) |
| |
| // Failed. |
| So(fetchProcFail("proc"), ShouldEqual, `unknown processor "proc"`) |
| }) |
| |
| Convey("runProcessorsTask broken package", func() { |
| impl.registerProcessor(&mockedProcessor{ |
| ProcID: "proc", |
| Result: processing.Result{Result: "must not be called"}, |
| }) |
| storeInstance([]string{"proc"}) |
| |
| cas.GetReaderImpl = func(_ context.Context, ref *api.ObjectRef) (gs.Reader, error) { |
| return testutil.NewMockGSReader([]byte("im not a zip")), nil |
| } |
| |
| err := impl.runProcessorsTask(ctx, &tasks.RunProcessors{Instance: inst}) |
| So(err, ShouldBeNil) |
| |
| // Failed. |
| So(fetchProcFail("proc"), ShouldEqual, `error when opening the package: zip: not a valid zip file`) |
| }) |
| |
| Convey("runProcessorsTask propagates transient proc errors", func() { |
| impl.registerProcessor(&mockedProcessor{ |
| ProcID: "good-proc", |
| Result: processing.Result{Result: goodResult}, |
| }) |
| impl.registerProcessor(&mockedProcessor{ |
| ProcID: "bad-proc", |
| Err: fmt.Errorf("failed transiently"), |
| }) |
| storeInstance([]string{"good-proc", "bad-proc"}) |
| |
| cas.GetReaderImpl = func(_ context.Context, ref *api.ObjectRef) (gs.Reader, error) { |
| return testutil.NewMockGSReader(testZip), nil |
| } |
| |
| err := impl.runProcessorsTask(ctx, &tasks.RunProcessors{Instance: inst}) |
| So(transient.Tag.In(err), ShouldBeTrue) |
| So(err, ShouldErrLike, "failed transiently") |
| |
| // bad-proc is still pending. |
| So(fetchInstance().ProcessorsPending, ShouldResemble, []string{"bad-proc"}) |
| // good-proc is done. |
| So(fetchProcSuccess("good-proc"), ShouldEqual, "OK") |
| }) |
| |
| Convey("runProcessorsTask handles fatal errors", func() { |
| impl.registerProcessor(&mockedProcessor{ |
| ProcID: "proc", |
| Result: processing.Result{Err: fmt.Errorf("boom")}, |
| }) |
| storeInstance([]string{"proc"}) |
| |
| cas.GetReaderImpl = func(_ context.Context, ref *api.ObjectRef) (gs.Reader, error) { |
| return testutil.NewMockGSReader(testZip), nil |
| } |
| |
| err := impl.runProcessorsTask(ctx, &tasks.RunProcessors{Instance: inst}) |
| So(err, ShouldBeNil) |
| |
| // Failed. |
| So(fetchProcFail("proc"), ShouldEqual, "boom") |
| }) |
| }) |
| } |
| |
| // mockedProcessor implements processing.Processor interface. |
| type mockedProcessor struct { |
| ProcID string |
| AppliesTo string |
| |
| RunCB func(*model.Instance, *processing.PackageReader) (processing.Result, error) |
| Result processing.Result |
| Err error |
| } |
| |
| func (m *mockedProcessor) ID() string { |
| return m.ProcID |
| } |
| |
| func (m *mockedProcessor) Applicable(ctx context.Context, inst *model.Instance) (bool, error) { |
| return inst.Package.StringID() == m.AppliesTo, nil |
| } |
| |
| func (m *mockedProcessor) Run(_ context.Context, i *model.Instance, r *processing.PackageReader) (processing.Result, error) { |
| if m.RunCB != nil { |
| return m.RunCB(i, r) |
| } |
| return m.Result, m.Err |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Instance listing and querying. |
| |
| func TestListInstances(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ts := time.Unix(1525136124, 0).UTC() |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| So(datastore.Put(ctx, &model.Package{Name: "a/b"}), ShouldBeNil) |
| So(datastore.Put(ctx, &model.Package{Name: "a/empty"}), ShouldBeNil) |
| |
| for i := 0; i < 4; i++ { |
| So(datastore.Put(ctx, &model.Instance{ |
| InstanceID: fmt.Sprintf("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%d", i), |
| Package: model.PackageKey(ctx, "a/b"), |
| RegisteredTs: ts.Add(time.Duration(i) * time.Minute), |
| }), ShouldBeNil) |
| } |
| |
| inst := func(i int) *api.Instance { |
| return &api.Instance{ |
| Package: "a/b", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: fmt.Sprintf("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%d", i), |
| }, |
| RegisteredTs: timestamppb.New(ts.Add(time.Duration(i) * time.Minute)), |
| } |
| } |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("Bad package name", func() { |
| _, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "///", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid package name") |
| }) |
| |
| Convey("Bad page size", func() { |
| _, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/b", |
| PageSize: -1, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "it should be non-negative") |
| }) |
| |
| Convey("Bad page token", func() { |
| _, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/b", |
| PageToken: "zzzz", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid cursor") |
| }) |
| |
| Convey("No access", func() { |
| _, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "z", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| }) |
| |
| Convey("No package", func() { |
| _, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/missing", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| }) |
| |
| Convey("Empty listing", func() { |
| res, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/empty", |
| }) |
| So(err, ShouldBeNil) |
| So(res, ShouldResembleProto, &api.ListInstancesResponse{}) |
| }) |
| |
| Convey("Full listing (no pagination)", func() { |
| res, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/b", |
| }) |
| So(err, ShouldBeNil) |
| So(res, ShouldResembleProto, &api.ListInstancesResponse{ |
| Instances: []*api.Instance{inst(3), inst(2), inst(1), inst(0)}, |
| }) |
| }) |
| |
| Convey("Listing with pagination", func() { |
| // First page. |
| res, err := impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/b", |
| PageSize: 3, |
| }) |
| So(err, ShouldBeNil) |
| So(res.Instances, ShouldResembleProto, []*api.Instance{ |
| inst(3), inst(2), inst(1), |
| }) |
| So(res.NextPageToken, ShouldNotEqual, "") |
| |
| // Second page. |
| res, err = impl.ListInstances(ctx, &api.ListInstancesRequest{ |
| Package: "a/b", |
| PageSize: 3, |
| PageToken: res.NextPageToken, |
| }) |
| So(err, ShouldBeNil) |
| So(res, ShouldResembleProto, &api.ListInstancesResponse{ |
| Instances: []*api.Instance{inst(0)}, |
| }) |
| }) |
| }) |
| } |
| |
| func TestSearchInstances(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| So(datastore.Put(ctx, &model.Package{Name: "a/b"}), ShouldBeNil) |
| |
| put := func(when int, iid string, tags ...string) { |
| inst := &model.Instance{ |
| InstanceID: iid, |
| Package: model.PackageKey(ctx, "a/b"), |
| RegisteredTs: testutil.TestTime.Add(time.Duration(when) * time.Second), |
| } |
| ents := make([]*model.Tag, len(tags)) |
| for i, t := range tags { |
| ents[i] = &model.Tag{ |
| ID: model.TagID(common.MustParseInstanceTag(t)), |
| Instance: datastore.KeyForObj(ctx, inst), |
| Tag: t, |
| RegisteredTs: testutil.TestTime.Add(time.Duration(when) * time.Second), |
| } |
| } |
| So(datastore.Put(ctx, inst, ents), ShouldBeNil) |
| } |
| |
| iid := func(i int) string { |
| ch := string([]byte{'0' + byte(i)}) |
| return strings.Repeat(ch, 40) |
| } |
| |
| ids := func(inst []*api.Instance) []string { |
| out := make([]string, len(inst)) |
| for i, obj := range inst { |
| out[i] = common.ObjectRefToInstanceID(obj.Instance) |
| } |
| return out |
| } |
| |
| expectedIIDs := make([]string, 10) |
| for i := 0; i < 10; i++ { |
| put(i, iid(i), "a:b") |
| expectedIIDs[9-i] = iid(i) // sorted by creation time, most recent first |
| } |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("Bad package name", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "///", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid package name") |
| }) |
| |
| Convey("Bad page size", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| PageSize: -1, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "it should be non-negative") |
| }) |
| |
| Convey("Bad page token", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| PageToken: "zzzz", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid cursor") |
| }) |
| |
| Convey("No tags specified", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'tags': cannot be empty") |
| }) |
| |
| Convey("Bad tag given", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| Tags: []*api.Tag{{Key: "", Value: "zz"}}, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `bad tag in 'tags': invalid tag key in ":zz"`) |
| }) |
| |
| Convey("No access", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "z", |
| Tags: []*api.Tag{{Key: "a", Value: "b"}}, |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| }) |
| |
| Convey("No package", func() { |
| _, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/missing", |
| Tags: []*api.Tag{{Key: "a", Value: "b"}}, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| }) |
| |
| Convey("Empty results", func() { |
| out, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| Tags: []*api.Tag{{Key: "a", Value: "missing"}}, |
| }) |
| So(err, ShouldBeNil) |
| So(ids(out.Instances), ShouldHaveLength, 0) |
| So(out.NextPageToken, ShouldEqual, "") |
| }) |
| |
| Convey("Full listing (no pagination)", func() { |
| out, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| Tags: []*api.Tag{{Key: "a", Value: "b"}}, |
| }) |
| So(err, ShouldBeNil) |
| So(ids(out.Instances), ShouldResemble, expectedIIDs) |
| So(out.NextPageToken, ShouldEqual, "") |
| }) |
| |
| Convey("Listing with pagination", func() { |
| out, err := impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| Tags: []*api.Tag{{Key: "a", Value: "b"}}, |
| PageSize: 6, |
| }) |
| So(err, ShouldBeNil) |
| So(ids(out.Instances), ShouldResemble, expectedIIDs[:6]) |
| So(out.NextPageToken, ShouldNotEqual, "") |
| |
| out, err = impl.SearchInstances(ctx, &api.SearchInstancesRequest{ |
| Package: "a/b", |
| Tags: []*api.Tag{{Key: "a", Value: "b"}}, |
| PageSize: 6, |
| PageToken: out.NextPageToken, |
| }) |
| So(err, ShouldBeNil) |
| So(ids(out.Instances), ShouldResemble, expectedIIDs[6:]) |
| So(out.NextPageToken, ShouldEqual, "") |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Refs support. |
| |
| func TestRefs(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("writer@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"user:writer@example.com"}, |
| }, |
| }, |
| }) |
| |
| putInst := func(pkg, iid string, pendingProcs, failedProcs []string) { |
| So(datastore.Put(ctx, |
| &model.Package{Name: pkg}, |
| &model.Instance{ |
| InstanceID: iid, |
| Package: model.PackageKey(ctx, pkg), |
| ProcessorsPending: pendingProcs, |
| ProcessorsFailure: failedProcs, |
| }), ShouldBeNil) |
| } |
| |
| digest := strings.Repeat("a", 40) |
| putInst("a/b/c", digest, nil, nil) |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("CreateRef/ListRefs/DeleteRef happy path", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(err, ShouldBeNil) |
| |
| // Can be listed now. |
| refs, err := impl.ListRefs(ctx, &api.ListRefsRequest{Package: "a/b/c"}) |
| So(err, ShouldBeNil) |
| So(refs.Refs, ShouldResembleProto, []*api.Ref{ |
| { |
| Name: "latest", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| ModifiedBy: "user:writer@example.com", |
| ModifiedTs: timestamppb.New(testutil.TestTime), |
| }, |
| }) |
| |
| _, err = impl.DeleteRef(ctx, &api.DeleteRefRequest{ |
| Name: "latest", |
| Package: "a/b/c", |
| }) |
| So(err, ShouldBeNil) |
| |
| // Missing now. |
| refs, err = impl.ListRefs(ctx, &api.ListRefsRequest{Package: "a/b/c"}) |
| So(err, ShouldBeNil) |
| So(refs.Refs, ShouldHaveLength, 0) |
| }) |
| |
| Convey("Bad ref", func() { |
| Convey("CreateRef", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "bad:ref:name", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'name'") |
| }) |
| Convey("DeleteRef", func() { |
| _, err := impl.DeleteRef(ctx, &api.DeleteRefRequest{ |
| Name: "bad:ref:name", |
| Package: "a/b/c", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'name'") |
| }) |
| }) |
| |
| Convey("Bad package name", func() { |
| Convey("CreateRef", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "///", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| Convey("DeleteRef", func() { |
| _, err := impl.DeleteRef(ctx, &api.DeleteRefRequest{ |
| Name: "latest", |
| Package: "///", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| Convey("ListRefs", func() { |
| _, err := impl.ListRefs(ctx, &api.ListRefsRequest{ |
| Package: "///", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| }) |
| |
| Convey("No access", func() { |
| Convey("CreateRef", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "z", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| Convey("DeleteRef", func() { |
| _, err := impl.DeleteRef(ctx, &api.DeleteRefRequest{ |
| Name: "latest", |
| Package: "z", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| Convey("ListRefs", func() { |
| _, err := impl.ListRefs(ctx, &api.ListRefsRequest{ |
| Package: "z", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| }) |
| |
| Convey("Missing package", func() { |
| Convey("CreateRef", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "a/b/z", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| Convey("DeleteRef", func() { |
| _, err := impl.DeleteRef(ctx, &api.DeleteRefRequest{ |
| Name: "latest", |
| Package: "a/b/z", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| Convey("ListRefs", func() { |
| _, err := impl.ListRefs(ctx, &api.ListRefsRequest{ |
| Package: "a/b/z", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| }) |
| |
| Convey("Bad instance", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: "123", |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| |
| Convey("Missing instance", func() { |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: strings.Repeat("b", 40), |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| |
| Convey("Instance is not ready yet", func() { |
| putInst("a/b/c", digest, []string{"proc"}, nil) |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.FailedPrecondition) |
| So(err, ShouldErrLike, "the instance is not ready yet, pending processors: proc") |
| }) |
| |
| Convey("Failed processors", func() { |
| putInst("a/b/c", digest, nil, []string{"proc"}) |
| _, err := impl.CreateRef(ctx, &api.Ref{ |
| Name: "latest", |
| Package: "a/b/c", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.Aborted) |
| So(err, ShouldErrLike, "some processors failed to process this instance: proc") |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tags support. |
| |
| func TestTags(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"user:writer@example.com"}, |
| }, |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:owner@example.com"}, |
| }, |
| }, |
| }) |
| |
| putInst := func(pkg, iid string, pendingProcs, failedProcs []string) *model.Instance { |
| inst := &model.Instance{ |
| InstanceID: iid, |
| Package: model.PackageKey(ctx, pkg), |
| ProcessorsPending: pendingProcs, |
| ProcessorsFailure: failedProcs, |
| } |
| So(datastore.Put(ctx, &model.Package{Name: pkg}, inst), ShouldBeNil) |
| return inst |
| } |
| |
| getTag := func(inst *model.Instance, tag string) *model.Tag { |
| t := &model.Tag{ |
| ID: model.TagID(common.MustParseInstanceTag(tag)), |
| Instance: datastore.KeyForObj(ctx, inst), |
| } |
| err := datastore.Get(ctx, t) |
| if err == datastore.ErrNoSuchEntity { |
| return nil |
| } |
| So(err, ShouldBeNil) |
| return t |
| } |
| |
| tags := func(t ...string) []*api.Tag { |
| out := make([]*api.Tag, len(t)) |
| for i, s := range t { |
| out[i] = common.MustParseInstanceTag(s) |
| } |
| return out |
| } |
| |
| digest := strings.Repeat("a", 40) |
| inst := putInst("a/b/c", digest, nil, nil) |
| objRef := &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| } |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("AttachTags/DetachTags happy path", func() { |
| _, err := impl.AttachTags(as("writer@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Tags: tags("a:0", "a:1"), |
| }) |
| So(err, ShouldBeNil) |
| |
| // Attached both. |
| So(getTag(inst, "a:0").RegisteredBy, ShouldEqual, "user:writer@example.com") |
| So(getTag(inst, "a:1").RegisteredBy, ShouldEqual, "user:writer@example.com") |
| |
| // Detaching requires OWNER. |
| _, err = impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Tags: tags("a:0", "a:1", "a:missing"), |
| }) |
| So(err, ShouldBeNil) |
| |
| // Missing now. |
| So(getTag(inst, "a:0"), ShouldBeNil) |
| So(getTag(inst, "a:1"), ShouldBeNil) |
| }) |
| |
| Convey("Bad package", func() { |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("owner@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b///", |
| Instance: objRef, |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| Convey("DetachTags", func() { |
| _, err := impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b///", |
| Instance: objRef, |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| }) |
| |
| Convey("Bad ObjectRef", func() { |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("owner@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/c", |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| Convey("DetachTags", func() { |
| _, err := impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/c", |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| }) |
| |
| Convey("Empty tag list", func() { |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("owner@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "cannot be empty") |
| }) |
| Convey("DetachTags", func() { |
| _, err := impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "cannot be empty") |
| }) |
| }) |
| |
| Convey("Bad tag", func() { |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("owner@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Tags: []*api.Tag{{Key: ":"}}, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `invalid tag key`) |
| }) |
| Convey("DetachTags", func() { |
| _, err := impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Tags: []*api.Tag{{Key: ":"}}, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `invalid tag key`) |
| }) |
| }) |
| |
| Convey("No access", func() { |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("reader@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Tags: tags("good:tag"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "has no required WRITER role") |
| }) |
| Convey("DetachTags", func() { |
| _, err := impl.DetachTags(as("writer@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Tags: tags("good:tag"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "has no required OWNER role") |
| }) |
| }) |
| |
| Convey("Missing package", func() { |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("owner@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/zzz", |
| Instance: objRef, |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| Convey("DetachTags", func() { |
| _, err := impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/zzz", |
| Instance: objRef, |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| }) |
| |
| Convey("Missing instance", func() { |
| missingRef := &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: strings.Repeat("b", 40), |
| } |
| Convey("AttachTags", func() { |
| _, err := impl.AttachTags(as("owner@example.com"), &api.AttachTagsRequest{ |
| Package: "a/b/c", |
| Instance: missingRef, |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| Convey("DetachTags", func() { |
| // DetachTags doesn't care. |
| _, err := impl.DetachTags(as("owner@example.com"), &api.DetachTagsRequest{ |
| Package: "a/b/c", |
| Instance: missingRef, |
| Tags: tags("a:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Instance metadata support. |
| |
| func TestInstanceMetadata(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| { |
| Role: api.Role_WRITER, |
| Principals: []string{"user:writer@example.com"}, |
| }, |
| { |
| Role: api.Role_OWNER, |
| Principals: []string{"user:owner@example.com"}, |
| }, |
| }, |
| }) |
| |
| putInst := func(pkg, iid string, pendingProcs, failedProcs []string) *model.Instance { |
| inst := &model.Instance{ |
| InstanceID: iid, |
| Package: model.PackageKey(ctx, pkg), |
| ProcessorsPending: pendingProcs, |
| ProcessorsFailure: failedProcs, |
| } |
| So(datastore.Put(ctx, &model.Package{Name: pkg}, inst), ShouldBeNil) |
| return inst |
| } |
| |
| getMD := func(inst *model.Instance, kv string) *model.InstanceMetadata { |
| split := strings.SplitN(kv, ":", 2) |
| t := &model.InstanceMetadata{ |
| Fingerprint: common.InstanceMetadataFingerprint(split[0], []byte(split[1])), |
| Instance: datastore.KeyForObj(ctx, inst), |
| } |
| if err := datastore.Get(ctx, t); err != datastore.ErrNoSuchEntity { |
| So(err, ShouldBeNil) |
| return t |
| } |
| return nil |
| } |
| |
| md := func(kv ...string) []*api.InstanceMetadata { |
| out := make([]*api.InstanceMetadata, len(kv)) |
| for i, s := range kv { |
| split := strings.SplitN(s, ":", 2) |
| out[i] = &api.InstanceMetadata{ |
| Key: split[0], |
| Value: []byte(split[1]), |
| } |
| } |
| return out |
| } |
| |
| digest := strings.Repeat("a", 40) |
| inst := putInst("a/b/c", digest, nil, nil) |
| objRef := &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: digest, |
| } |
| |
| impl := repoImpl{meta: &meta} |
| |
| Convey("AttachMetadata/DetachMetadata/ListMetadata happy path", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("k0:0", "k1:1"), |
| }) |
| So(err, ShouldBeNil) |
| |
| // Attached both. |
| So(getMD(inst, "k0:0").AttachedBy, ShouldEqual, "user:writer@example.com") |
| So(getMD(inst, "k1:1").AttachedBy, ShouldEqual, "user:writer@example.com") |
| |
| // Can retrieve it all. |
| resp, err := impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| }) |
| So(err, ShouldBeNil) |
| So(resp.Metadata, ShouldHaveLength, 2) |
| |
| // Can retrieve an individual key. |
| resp, err = impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Keys: []string{"k0"}, |
| }) |
| So(err, ShouldBeNil) |
| So(resp.Metadata, ShouldHaveLength, 1) |
| |
| // Detaching requires OWNER. Detach one by giving a KV pair, and another |
| // via its fingerprint. |
| _, err = impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: []*api.InstanceMetadata{ |
| { |
| Key: "k0", |
| Value: []byte{'0'}, |
| }, |
| { |
| Fingerprint: common.InstanceMetadataFingerprint("k1", []byte{'1'}), |
| }, |
| }, |
| }) |
| So(err, ShouldBeNil) |
| |
| // Missing now. |
| So(getMD(inst, "k0:0"), ShouldBeNil) |
| So(getMD(inst, "k1:1"), ShouldBeNil) |
| }) |
| |
| Convey("Bad package", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b//", |
| Instance: objRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b//", |
| Instance: objRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| Convey("ListMetadata", func() { |
| _, err := impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b//", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| }) |
| |
| Convey("Bad ObjectRef", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| Convey("ListMetadata", func() { |
| _, err := impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| }) |
| |
| Convey("Empty metadata list", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "cannot be empty") |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "cannot be empty") |
| }) |
| }) |
| |
| Convey("Bad metadata key", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("ZZZ:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `invalid metadata key`) |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("ZZZ:0"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `invalid metadata key`) |
| }) |
| Convey("ListMetadata", func() { |
| _, err := impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Keys: []string{"ZZZ"}, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `invalid metadata key`) |
| }) |
| }) |
| |
| Convey("Bad metadata value", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("k:" + strings.Repeat("z", 512*1024+1)), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `metadata with key "k": the metadata value is too long`) |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("k:" + strings.Repeat("z", 512*1024+1)), |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `metadata with key "k": the metadata value is too long`) |
| }) |
| }) |
| |
| Convey("Bad metadata content type in AttachMetadata", func() { |
| m := md("k:0") |
| m[0].ContentType = "zzz zzz" |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: m, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `metadata with key "k": bad content type "zzz zzz`) |
| }) |
| |
| Convey("Bad fingerprint in DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: []*api.InstanceMetadata{ |
| {Fingerprint: "bad"}, |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, `bad metadata fingerprint "bad"`) |
| }) |
| |
| Convey("No access", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("reader@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "has no required WRITER role") |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("writer@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "has no required OWNER role") |
| }) |
| Convey("ListMetadata", func() { |
| _, err := impl.ListMetadata(as("unknown@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| }) |
| |
| Convey("Missing package", func() { |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c/missing", |
| Instance: objRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c/missing", |
| Instance: objRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| Convey("ListMetadata", func() { |
| _, err := impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c/missing", |
| Instance: objRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| }) |
| |
| Convey("Missing instance", func() { |
| missingRef := &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: strings.Repeat("b", 40), |
| } |
| Convey("AttachMetadata", func() { |
| _, err := impl.AttachMetadata(as("writer@example.com"), &api.AttachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: missingRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| Convey("DetachMetadata", func() { |
| _, err := impl.DetachMetadata(as("owner@example.com"), &api.DetachMetadataRequest{ |
| Package: "a/b/c", |
| Instance: missingRef, |
| Metadata: md("k:0", "k:1"), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| Convey("ListMetadata", func() { |
| _, err := impl.ListMetadata(as("reader@example.com"), &api.ListMetadataRequest{ |
| Package: "a/b/c", |
| Instance: missingRef, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Version resolution and instance info fetching. |
| |
| func TestResolveVersion(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| impl := repoImpl{meta: &meta} |
| |
| pkg := &model.Package{Name: "a/pkg"} |
| inst1 := &model.Instance{ |
| InstanceID: strings.Repeat("1", 40), |
| Package: model.PackageKey(ctx, "a/pkg"), |
| RegisteredBy: "user:1@example.com", |
| } |
| inst2 := &model.Instance{ |
| InstanceID: strings.Repeat("2", 40), |
| Package: model.PackageKey(ctx, "a/pkg"), |
| RegisteredBy: "user:2@example.com", |
| } |
| |
| So(datastore.Put(ctx, pkg, inst1, inst2), ShouldBeNil) |
| So(model.SetRef(ctx, "latest", inst2), ShouldBeNil) |
| So(model.AttachTags(ctx, inst1, []*api.Tag{ |
| {Key: "ver", Value: "1"}, |
| {Key: "ver", Value: "ambiguous"}, |
| }), ShouldBeNil) |
| So(model.AttachTags(ctx, inst2, []*api.Tag{ |
| {Key: "ver", Value: "2"}, |
| {Key: "ver", Value: "ambiguous"}, |
| }), ShouldBeNil) |
| |
| Convey("Happy path", func() { |
| inst, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldBeNil) |
| So(inst, ShouldResembleProto, inst2.Proto()) |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "///", |
| Version: "latest", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| |
| Convey("Bad version name", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/pkg", |
| Version: "::", |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'version'") |
| }) |
| |
| Convey("No access", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "b", |
| Version: "latest", |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| |
| Convey("Missing package", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/b", |
| Version: "latest", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| |
| Convey("Missing instance", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/pkg", |
| Version: strings.Repeat("f", 40), |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| |
| Convey("Missing ref", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/pkg", |
| Version: "missing", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such ref") |
| }) |
| |
| Convey("Missing tag", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/pkg", |
| Version: "ver:missing", |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such tag") |
| }) |
| |
| Convey("Ambiguous tag", func() { |
| _, err := impl.ResolveVersion(ctx, &api.ResolveVersionRequest{ |
| Package: "a/pkg", |
| Version: "ver:ambiguous", |
| }) |
| So(status.Code(err), ShouldEqual, codes.FailedPrecondition) |
| So(err, ShouldErrLike, "ambiguity when resolving the tag") |
| }) |
| }) |
| } |
| |
| func TestGetInstanceURLAndDownloads(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| cas := testutil.MockCAS{} |
| impl := repoImpl{meta: &meta, cas: &cas} |
| |
| inst := &model.Instance{ |
| InstanceID: strings.Repeat("1", 40), |
| Package: model.PackageKey(ctx, "a/pkg"), |
| RegisteredBy: "user:1@example.com", |
| } |
| So(datastore.Put(ctx, &model.Package{Name: "a/pkg"}, inst), ShouldBeNil) |
| |
| cas.GetObjectURLImpl = func(_ context.Context, r *api.GetObjectURLRequest) (*api.ObjectURL, error) { |
| So(r.Object.HashAlgo, ShouldEqual, api.HashAlgo_SHA1) |
| So(r.Object.HexDigest, ShouldEqual, inst.InstanceID) |
| return &api.ObjectURL{ |
| SignedUrl: fmt.Sprintf("http://example.com/%s?d=%s", r.Object.HexDigest, r.DownloadFilename), |
| }, nil |
| } |
| |
| Convey("GetInstanceURL", func() { |
| Convey("Happy path", func() { |
| resp, err := impl.GetInstanceURL(ctx, &api.GetInstanceURLRequest{ |
| Package: inst.Package.StringID(), |
| Instance: inst.Proto().Instance, |
| }) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.ObjectURL{ |
| SignedUrl: "http://example.com/1111111111111111111111111111111111111111?d=", |
| }) |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := impl.GetInstanceURL(ctx, &api.GetInstanceURLRequest{ |
| Package: "///", |
| Instance: inst.Proto().Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| |
| Convey("Bad instance", func() { |
| _, err := impl.GetInstanceURL(ctx, &api.GetInstanceURLRequest{ |
| Package: "a/pkg", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: "huh", |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| |
| Convey("No access", func() { |
| _, err := impl.GetInstanceURL(ctx, &api.GetInstanceURLRequest{ |
| Package: "b", |
| Instance: inst.Proto().Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| |
| Convey("Missing package", func() { |
| _, err := impl.GetInstanceURL(ctx, &api.GetInstanceURLRequest{ |
| Package: "a/missing", |
| Instance: inst.Proto().Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| |
| Convey("Missing instance", func() { |
| _, err := impl.GetInstanceURL(ctx, &api.GetInstanceURLRequest{ |
| Package: "a/pkg", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: strings.Repeat("f", 40), |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| }) |
| |
| Convey("Raw download handler", func() { |
| call := func(path string) *httptest.ResponseRecorder { |
| rr := httptest.NewRecorder() |
| adaptGrpcErr(impl.handlePackageDownload)(&router.Context{ |
| Request: (&http.Request{}).WithContext(ctx), |
| Params: httprouter.Params{ |
| {Key: "path", Value: path}, |
| }, |
| Writer: rr, |
| }) |
| return rr |
| } |
| |
| Convey("Happy path", func() { |
| rr := call("/a/pkg/+/1111111111111111111111111111111111111111") |
| So(rr.Code, ShouldEqual, http.StatusFound) |
| So(rr.Header().Get("Location"), ShouldEqual, "http://example.com/1111111111111111111111111111111111111111?d=a-pkg.zip") |
| So(rr.Header().Get(cipdInstanceHeader), ShouldEqual, inst.InstanceID) |
| }) |
| |
| Convey("Malformed URL", func() { |
| rr := call("huh") |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "the URL should have form") |
| }) |
| |
| Convey("Bad package name", func() { |
| rr := call("/???/+/live") |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "invalid package name") |
| }) |
| |
| Convey("Bad version", func() { |
| rr := call("/pkg/+/???") |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "bad version") |
| }) |
| |
| Convey("No access", func() { |
| rr := call("/b/+/live") |
| So(rr.Code, ShouldEqual, http.StatusForbidden) |
| So(rr.Body.String(), ShouldContainSubstring, "is not allowed to see it") |
| }) |
| |
| Convey("Missing package", func() { |
| rr := call("/a/missing/+/live") |
| So(rr.Code, ShouldEqual, http.StatusNotFound) |
| So(rr.Body.String(), ShouldContainSubstring, "no such package") |
| }) |
| |
| Convey("Missing instance", func() { |
| rr := call("/a/pkg/+/1111111111111111111111111111111111111112") |
| So(rr.Code, ShouldEqual, http.StatusNotFound) |
| So(rr.Body.String(), ShouldContainSubstring, "no such instance") |
| }) |
| }) |
| }) |
| } |
| |
| func TestDescribeInstance(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| inst := &model.Instance{ |
| InstanceID: strings.Repeat("1", 40), |
| Package: model.PackageKey(ctx, "a/pkg"), |
| RegisteredBy: "user:1@example.com", |
| } |
| So(datastore.Put(ctx, &model.Package{Name: "a/pkg"}, inst), ShouldBeNil) |
| |
| Convey("Happy path, basic info", func() { |
| resp, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "a/pkg", |
| Instance: inst.Proto().Instance, |
| }) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.DescribeInstanceResponse{ |
| Instance: inst.Proto(), |
| }) |
| }) |
| |
| Convey("Happy path, full info", func() { |
| model.AttachTags(as("tag@example.com"), inst, []*api.Tag{ |
| {Key: "a", Value: "0"}, |
| {Key: "a", Value: "1"}, |
| }) |
| |
| model.SetRef(as("ref@example.com"), "ref_a", inst) |
| model.SetRef(as("ref@example.com"), "ref_b", inst) |
| |
| model.AttachMetadata(as("metadata@example.com"), inst, []*api.InstanceMetadata{ |
| { |
| Key: "foo", |
| Value: []byte("bar"), |
| ContentType: "image/png", |
| }, |
| { |
| Key: "bar", |
| Value: []byte("baz"), |
| ContentType: "text/plain", |
| }, |
| }) |
| |
| inst.ProcessorsSuccess = []string{"proc"} |
| datastore.Put(ctx, inst, &model.ProcessingResult{ |
| ProcID: "proc", |
| Instance: datastore.KeyForObj(ctx, inst), |
| Success: true, |
| CreatedTs: testutil.TestTime, |
| }) |
| |
| resp, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "a/pkg", |
| Instance: inst.Proto().Instance, |
| DescribeTags: true, |
| DescribeRefs: true, |
| DescribeProcessors: true, |
| DescribeMetadata: true, |
| }) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.DescribeInstanceResponse{ |
| Instance: inst.Proto(), |
| Tags: []*api.Tag{ |
| { |
| Key: "a", |
| Value: "0", |
| AttachedBy: "user:tag@example.com", |
| AttachedTs: timestamppb.New(testutil.TestTime), |
| }, |
| { |
| Key: "a", |
| Value: "1", |
| AttachedBy: "user:tag@example.com", |
| AttachedTs: timestamppb.New(testutil.TestTime), |
| }, |
| }, |
| Refs: []*api.Ref{ |
| { |
| Name: "ref_a", |
| Package: "a/pkg", |
| Instance: inst.Proto().Instance, |
| ModifiedBy: "user:ref@example.com", |
| ModifiedTs: timestamppb.New(testutil.TestTime), |
| }, |
| { |
| Name: "ref_b", |
| Package: "a/pkg", |
| Instance: inst.Proto().Instance, |
| ModifiedBy: "user:ref@example.com", |
| ModifiedTs: timestamppb.New(testutil.TestTime), |
| }, |
| }, |
| Processors: []*api.Processor{ |
| { |
| Id: "proc", |
| State: api.Processor_SUCCEEDED, |
| FinishedTs: timestamppb.New(testutil.TestTime), |
| }, |
| }, |
| // Note that metadata is returned LIFO. |
| Metadata: []*api.InstanceMetadata{ |
| { |
| Key: "bar", |
| Value: []byte("baz"), |
| ContentType: "text/plain", |
| Fingerprint: "aec8a983ee3429852adfcfecacad886d", |
| AttachedBy: "user:metadata@example.com", |
| AttachedTs: timestamppb.New(testutil.TestTime), |
| }, |
| { |
| Key: "foo", |
| Value: []byte("bar"), |
| ContentType: "image/png", |
| Fingerprint: "a765a8beaa9d561d4c5cbed29d8f4e30", |
| AttachedBy: "user:metadata@example.com", |
| AttachedTs: timestamppb.New(testutil.TestTime), |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "///", |
| Instance: inst.Proto().Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'package'") |
| }) |
| |
| Convey("Bad instance", func() { |
| _, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "a/pkg", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: "huh", |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "bad 'instance'") |
| }) |
| |
| Convey("No access", func() { |
| _, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "b", |
| Instance: inst.Proto().Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "is not allowed to see it") |
| }) |
| |
| Convey("Missing package", func() { |
| _, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "a/missing", |
| Instance: inst.Proto().Instance, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such package") |
| }) |
| |
| Convey("Missing instance", func() { |
| _, err := impl.DescribeInstance(ctx, &api.DescribeInstanceRequest{ |
| Package: "a/pkg", |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: strings.Repeat("f", 40), |
| }, |
| }) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| }) |
| } |
| |
| func TestDescribeBootstrapBundle(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| impl := repoImpl{meta: &meta} |
| |
| putInst := func(pkg, extracted string) { |
| inst := &model.Instance{ |
| InstanceID: common.ObjectRefToInstanceID(&api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: strings.Repeat("1", 64), |
| }), |
| Package: model.PackageKey(ctx, pkg), |
| RegisteredBy: "user:1@example.com", |
| } |
| if extracted != "" { |
| r := &model.ProcessingResult{ |
| ProcID: processing.BootstrapPackageExtractorProcID, |
| Instance: datastore.KeyForObj(ctx, inst), |
| } |
| if extracted != "BROKEN" { |
| r.Success = true |
| r.WriteResult(processing.BootstrapExtractorResult{ |
| File: extracted, |
| HashAlgo: "SHA256", |
| HashDigest: strings.Repeat("a", 64), |
| Size: 12345, |
| }) |
| inst.ProcessorsSuccess = []string{processing.BootstrapPackageExtractorProcID} |
| } else { |
| r.Error = "Extraction broken" |
| inst.ProcessorsFailure = []string{processing.BootstrapPackageExtractorProcID} |
| } |
| So(datastore.Put(ctx, r), ShouldBeNil) |
| } |
| So(datastore.Put(ctx, |
| &model.Package{Name: pkg}, |
| inst, |
| &model.Ref{Name: "latest", Package: inst.Package, InstanceID: inst.InstanceID}, |
| ), ShouldBeNil) |
| datastore.GetTestable(ctx).CatchupIndexes() |
| } |
| |
| expectedFile := func(pkg, extracted string) *api.DescribeBootstrapBundleResponse_BootstrapFile { |
| return &api.DescribeBootstrapBundleResponse_BootstrapFile{ |
| Package: pkg, |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: strings.Repeat("1", 64), |
| }, |
| File: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: strings.Repeat("a", 64), |
| }, |
| Name: extracted, |
| Size: 12345, |
| } |
| } |
| |
| expectedError := func(pkg string, code codes.Code, msg string) *api.DescribeBootstrapBundleResponse_BootstrapFile { |
| return &api.DescribeBootstrapBundleResponse_BootstrapFile{ |
| Package: pkg, |
| Status: &statuspb.Status{ |
| Code: int32(code), |
| Message: msg, |
| }, |
| } |
| } |
| |
| Convey("Happy path", func() { |
| putInst("a/pkg/var-1", "file1") |
| putInst("a/pkg/var-2", "file2") |
| putInst("a/pkg/var-3", "file3") |
| putInst("a/pkg", "") // will be ignored |
| putInst("a/pkg/sun/package", "") // will be ignored |
| |
| Convey("With prefix listing", func() { |
| resp, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.DescribeBootstrapBundleResponse{ |
| Files: []*api.DescribeBootstrapBundleResponse_BootstrapFile{ |
| expectedFile("a/pkg/var-1", "file1"), |
| expectedFile("a/pkg/var-2", "file2"), |
| expectedFile("a/pkg/var-3", "file3"), |
| }, |
| }) |
| }) |
| |
| Convey("With variants", func() { |
| resp, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Variants: []string{"var-3", "var-1"}, |
| Version: "latest", |
| }) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.DescribeBootstrapBundleResponse{ |
| Files: []*api.DescribeBootstrapBundleResponse_BootstrapFile{ |
| expectedFile("a/pkg/var-3", "file3"), |
| expectedFile("a/pkg/var-1", "file1"), |
| }, |
| }) |
| }) |
| }) |
| |
| Convey("Partial failure", func() { |
| putInst("a/pkg/var-1", "file1") |
| putInst("a/pkg/var-2", "") |
| |
| resp, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.DescribeBootstrapBundleResponse{ |
| Files: []*api.DescribeBootstrapBundleResponse_BootstrapFile{ |
| expectedFile("a/pkg/var-1", "file1"), |
| expectedError("a/pkg/var-2", codes.FailedPrecondition, `"a/pkg/var-2" is not a bootstrap package`), |
| }, |
| }) |
| }) |
| |
| Convey("Total failure", func() { |
| putInst("a/pkg/var-1", "") |
| putInst("a/pkg/var-2", "") |
| putInst("a/pkg/var-3", "") |
| |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.FailedPrecondition, |
| `"a/pkg/var-1" is not a bootstrap package (and 2 other errors like this)`) |
| }) |
| |
| Convey("Empty prefix", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.NotFound, `no packages directly under prefix "a/pkg"`) |
| }) |
| |
| Convey("Missing version", func() { |
| putInst("a/pkg/var-1", "file1") |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "not-latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.NotFound, `no such ref`) |
| }) |
| |
| Convey("Broken processor", func() { |
| putInst("a/pkg/var-1", "BROKEN") |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.Aborted, `some processors failed to process this instance`) |
| }) |
| |
| Convey("Request validation", func() { |
| putInst("a/pkg/var-1", "file1") |
| |
| Convey("Bad prefix format", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "///", |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.InvalidArgument) |
| }) |
| |
| Convey("Variant with /", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Variants: []string{"some/thing"}, |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.InvalidArgument) |
| }) |
| |
| Convey("Empty variant", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Variants: []string{""}, |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.InvalidArgument) |
| }) |
| |
| Convey("Malformed variant name", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Variants: []string{"BAD"}, |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.InvalidArgument) |
| }) |
| |
| Convey("Duplicate variants", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Variants: []string{"var-1", "var-1"}, |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.InvalidArgument) |
| }) |
| |
| Convey("Bad version", func() { |
| _, err := impl.DescribeBootstrapBundle(ctx, &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "bad version ID", |
| }) |
| So(err, ShouldHaveRPCCode, codes.InvalidArgument) |
| }) |
| |
| Convey("Not a reader", func() { |
| _, err := impl.DescribeBootstrapBundle(as("someone@example.com"), &api.DescribeBootstrapBundleRequest{ |
| Prefix: "a/pkg", |
| Version: "latest", |
| }) |
| So(err, ShouldHaveRPCCode, codes.PermissionDenied) |
| }) |
| }) |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Client bootstrap and legacy API. |
| |
| func TestClientBootstrap(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| cas := testutil.MockCAS{ |
| GetObjectURLImpl: func(_ context.Context, r *api.GetObjectURLRequest) (*api.ObjectURL, error) { |
| return &api.ObjectURL{ |
| SignedUrl: fmt.Sprintf("http://fake/%s/%s?d=%s&blah=zzz", r.Object.HashAlgo, r.Object.HexDigest, r.DownloadFilename), |
| }, nil |
| }, |
| } |
| |
| impl := repoImpl{meta: &meta, cas: &cas} |
| |
| goodPlat := "linux-amd64" |
| goodDigest := strings.Repeat("f", 64) |
| goodIID := common.ObjectRefToInstanceID(&api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: goodDigest, |
| }) |
| goodPkg, err := processing.GetClientPackage(goodPlat) |
| So(err, ShouldBeNil) |
| |
| setup := func(res *processing.ClientExtractorResult, fail string) (*model.Instance, *model.ProcessingResult) { |
| pkgName, err := processing.GetClientPackage(goodPlat) |
| So(err, ShouldBeNil) |
| pkg := &model.Package{Name: pkgName} |
| inst := &model.Instance{ |
| InstanceID: goodIID, |
| Package: datastore.KeyForObj(ctx, pkg), |
| } |
| proc := &model.ProcessingResult{ |
| ProcID: processing.ClientExtractorProcID, |
| Instance: datastore.KeyForObj(ctx, inst), |
| } |
| if res != nil { |
| proc.Success = true |
| proc.WriteResult(res) |
| inst.ProcessorsSuccess = []string{proc.ProcID} |
| } else { |
| proc.Error = fail |
| inst.ProcessorsFailure = []string{proc.ProcID} |
| } |
| So(datastore.Put(ctx, pkg, inst, proc), ShouldBeNil) |
| return inst, proc |
| } |
| |
| res := processing.ClientExtractorResult{} |
| res.ClientBinary.HashAlgo = "SHA256" |
| res.ClientBinary.HashDigest = strings.Repeat("b", 64) |
| res.ClientBinary.AllHashDigests = map[string]string{ |
| "SHA1": strings.Repeat("c", 40), |
| "SHA256": strings.Repeat("b", 64), |
| } |
| res.ClientBinary.Size = 123456789101112 |
| inst, proc := setup(&res, "") |
| |
| expectedClientURL := fmt.Sprintf("http://fake/%s/%s?d=cipd&blah=zzz", res.ClientBinary.HashAlgo, res.ClientBinary.HashDigest) |
| |
| Convey("Bootstrap endpoint", func() { |
| call := func(plat, ver string) *httptest.ResponseRecorder { |
| form := url.Values{} |
| form.Add("platform", plat) |
| form.Add("version", ver) |
| rr := httptest.NewRecorder() |
| adaptGrpcErr(impl.handleClientBootstrap)(&router.Context{ |
| Request: (&http.Request{Form: form}).WithContext(ctx), |
| Writer: rr, |
| }) |
| return rr |
| } |
| |
| Convey("Happy path", func() { |
| rr := call(goodPlat, goodIID) |
| So(rr.Code, ShouldEqual, http.StatusFound) |
| So(rr.Header().Get("Location"), ShouldEqual, expectedClientURL) |
| So(rr.Header().Get(cipdInstanceHeader), ShouldEqual, goodIID) |
| }) |
| |
| Convey("No plat", func() { |
| rr := call("", goodIID) |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "no 'platform' specified") |
| }) |
| |
| Convey("Bad plat", func() { |
| rr := call("...", goodIID) |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "bad platform name") |
| }) |
| |
| Convey("No ver", func() { |
| rr := call(goodPlat, "") |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "no 'version' specified") |
| }) |
| |
| Convey("Bad ver", func() { |
| rr := call(goodPlat, "!!!!") |
| So(rr.Code, ShouldEqual, http.StatusBadRequest) |
| So(rr.Body.String(), ShouldContainSubstring, "bad version") |
| }) |
| |
| Convey("No access", func() { |
| ctx = as("someone@example.com") |
| rr := call(goodPlat, goodIID) |
| So(rr.Code, ShouldEqual, http.StatusForbidden) |
| So(rr.Body.String(), ShouldContainSubstring, "is not allowed to see it") |
| }) |
| |
| Convey("Missing ver", func() { |
| rr := call(goodPlat, "missing") |
| So(rr.Code, ShouldEqual, http.StatusNotFound) |
| So(rr.Body.String(), ShouldContainSubstring, "no such ref") |
| }) |
| |
| Convey("Missing instance ID", func() { |
| rr := call(goodPlat, strings.Repeat("b", 40)) |
| So(rr.Code, ShouldEqual, http.StatusNotFound) |
| So(rr.Body.String(), ShouldContainSubstring, "no such instance") |
| }) |
| |
| Convey("Not extracted yet", func() { |
| inst.ProcessorsPending = []string{proc.ProcID} |
| datastore.Delete(ctx, proc) |
| datastore.Put(ctx, inst) |
| |
| rr := call(goodPlat, goodIID) |
| So(rr.Code, ShouldEqual, http.StatusNotFound) |
| So(rr.Body.String(), ShouldContainSubstring, "is not extracted yet") |
| }) |
| |
| Convey("Fatal error during extraction", func() { |
| setup(nil, "BOOM") |
| |
| rr := call(goodPlat, goodIID) |
| So(rr.Code, ShouldEqual, http.StatusNotFound) |
| So(rr.Body.String(), ShouldContainSubstring, "BOOM") |
| }) |
| }) |
| |
| Convey("DescribeClient RPC", func() { |
| call := func(pkg, sha256Digest string) (*api.DescribeClientResponse, error) { |
| return impl.DescribeClient(ctx, &api.DescribeClientRequest{ |
| Package: pkg, |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: sha256Digest, |
| }, |
| }) |
| } |
| |
| Convey("Happy path", func() { |
| resp, err := call(goodPkg, goodDigest) |
| So(err, ShouldBeNil) |
| So(resp, ShouldResembleProto, &api.DescribeClientResponse{ |
| Instance: &api.Instance{ |
| Package: goodPkg, |
| Instance: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: goodDigest, |
| }, |
| }, |
| ClientRef: &api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: res.ClientBinary.AllHashDigests["SHA256"], |
| }, |
| ClientBinary: &api.ObjectURL{ |
| SignedUrl: expectedClientURL, |
| }, |
| ClientSize: res.ClientBinary.Size, |
| LegacySha1: res.ClientBinary.AllHashDigests["SHA1"], |
| ClientRefAliases: []*api.ObjectRef{ |
| { |
| HashAlgo: api.HashAlgo_SHA1, |
| HexDigest: res.ClientBinary.AllHashDigests["SHA1"], |
| }, |
| { |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: res.ClientBinary.AllHashDigests["SHA256"], |
| }, |
| }, |
| }) |
| }) |
| |
| Convey("Bad package name", func() { |
| _, err := call("not/a/client", goodDigest) |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "not a CIPD client package") |
| }) |
| |
| Convey("Bad instance ref", func() { |
| _, err := call(goodPkg, "not-an-id") |
| So(status.Code(err), ShouldEqual, codes.InvalidArgument) |
| So(err, ShouldErrLike, "invalid SHA256 hex digest") |
| }) |
| |
| Convey("Missing instance", func() { |
| _, err := call(goodPkg, strings.Repeat("e", 64)) |
| So(status.Code(err), ShouldEqual, codes.NotFound) |
| So(err, ShouldErrLike, "no such instance") |
| }) |
| |
| Convey("No access", func() { |
| ctx = as("someone@example.com") |
| _, err := call(goodPkg, goodDigest) |
| So(status.Code(err), ShouldEqual, codes.PermissionDenied) |
| So(err, ShouldErrLike, "not allowed to see it") |
| }) |
| |
| Convey("Not extracted yet", func() { |
| inst.ProcessorsPending = []string{proc.ProcID} |
| datastore.Delete(ctx, proc) |
| datastore.Put(ctx, inst) |
| |
| _, err := call(goodPkg, goodDigest) |
| So(status.Code(err), ShouldEqual, codes.FailedPrecondition) |
| So(err, ShouldErrLike, "the instance is not ready yet") |
| }) |
| |
| Convey("Fatal error during extraction", func() { |
| setup(nil, "BOOM") |
| |
| _, err := call(goodPkg, goodDigest) |
| So(status.Code(err), ShouldEqual, codes.Aborted) |
| So(err, ShouldErrLike, "some processors failed to process this instance") |
| }) |
| }) |
| |
| Convey("Legacy API", func() { |
| call := func(pkg, iid, ct string) (code int, body string) { |
| rr := httptest.NewRecorder() |
| adaptGrpcErr(impl.handleLegacyClientInfo)(&router.Context{ |
| Request: (&http.Request{Form: url.Values{ |
| "package_name": {pkg}, |
| "instance_id": {iid}, |
| }}).WithContext(ctx), |
| Writer: rr, |
| }) |
| expCT := "text/plain; charset=utf-8" |
| if ct == "json" { |
| expCT = "application/json; charset=utf-8" |
| } |
| So(rr.Header().Get("Content-Type"), ShouldEqual, expCT) |
| code = rr.Code |
| body = strings.TrimSpace(rr.Body.String()) |
| return |
| } |
| |
| Convey("Happy path", func() { |
| code, body := call(goodPkg, goodIID, "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, |
| fmt.Sprintf(`{ |
| "client_binary": { |
| "fetch_url": "%s", |
| "file_name": "cipd", |
| "sha1": "%s", |
| "size": "123456789101112" |
| }, |
| "instance": { |
| "package_name": "infra/tools/cipd/linux-amd64", |
| "instance_id": "%s" |
| }, |
| "status": "SUCCESS" |
| }`, |
| expectedClientURL, |
| res.ClientBinary.AllHashDigests["SHA1"], |
| inst.InstanceID)) |
| }) |
| |
| Convey("Bad package name", func() { |
| code, body := call("not/a/client", goodIID, "text") |
| So(code, ShouldEqual, http.StatusBadRequest) |
| So(body, ShouldContainSubstring, "not a CIPD client package") |
| }) |
| |
| Convey("Bad instance ID", func() { |
| code, body := call(goodPkg, "not-an-id", "text") |
| So(code, ShouldEqual, http.StatusBadRequest) |
| So(body, ShouldContainSubstring, "not a valid package instance ID") |
| }) |
| |
| Convey("Missing instance", func() { |
| badIID := common.ObjectRefToInstanceID(&api.ObjectRef{ |
| HashAlgo: api.HashAlgo_SHA256, |
| HexDigest: strings.Repeat("d", 64), |
| }) |
| code, body := call(goodPkg, badIID, "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "error_message": "no such instance", |
| "status": "INSTANCE_NOT_FOUND" |
| }`) |
| }) |
| |
| Convey("No access", func() { |
| ctx = as("someone@example.com") |
| code, body := call(goodPkg, goodIID, "text") |
| So(code, ShouldEqual, http.StatusForbidden) |
| So(body, ShouldContainSubstring, "not allowed to see it") |
| }) |
| |
| Convey("Not extracted yet", func() { |
| inst.ProcessorsPending = []string{proc.ProcID} |
| datastore.Delete(ctx, proc) |
| datastore.Put(ctx, inst) |
| |
| code, body := call(goodPkg, goodIID, "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "error_message": "the client binary is not extracted yet, try later", |
| "status": "NOT_EXTRACTED_YET" |
| }`) |
| }) |
| |
| Convey("Fatal error during extraction", func() { |
| setup(nil, "BOOM") |
| |
| code, body := call(goodPkg, goodIID, "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "error_message": "the client binary is not available: some processors failed to process this instance: cipd_client_binary:v1", |
| "status": "ERROR" |
| }`) |
| }) |
| }) |
| }) |
| } |
| |
| func TestLegacyHandlers(t *testing.T) { |
| t.Parallel() |
| |
| Convey("With fakes", t, func() { |
| ctx, _, as := testutil.TestingContext() |
| ctx = as("reader@example.com") |
| |
| meta := testutil.MetadataStore{} |
| meta.Populate("a", &api.PrefixMetadata{ |
| Acls: []*api.PrefixMetadata_ACL{ |
| { |
| Role: api.Role_READER, |
| Principals: []string{"user:reader@example.com"}, |
| }, |
| }, |
| }) |
| |
| cas := testutil.MockCAS{ |
| GetObjectURLImpl: func(_ context.Context, r *api.GetObjectURLRequest) (*api.ObjectURL, error) { |
| return &api.ObjectURL{ |
| SignedUrl: "http://fake/" + common.ObjectRefToInstanceID(r.Object), |
| }, nil |
| }, |
| } |
| impl := repoImpl{meta: &meta, cas: &cas} |
| |
| inst1 := &model.Instance{ |
| InstanceID: strings.Repeat("a", 40), |
| Package: model.PackageKey(ctx, "a/b"), |
| RegisteredBy: "user:reg@example.com", |
| RegisteredTs: testutil.TestTime, |
| } |
| inst2 := &model.Instance{ |
| InstanceID: strings.Repeat("b", 40), |
| Package: model.PackageKey(ctx, "a/b"), |
| } |
| So(datastore.Put(ctx, &model.Package{Name: "a/b"}, inst1, inst2), ShouldBeNil) |
| |
| // Make an ambiguous tag. |
| model.AttachTags(ctx, inst1, []*api.Tag{{Key: "k", Value: "v"}}) |
| model.AttachTags(ctx, inst2, []*api.Tag{{Key: "k", Value: "v"}}) |
| |
| callHandler := func(h router.Handler, f url.Values, ct string) (code int, body string) { |
| rr := httptest.NewRecorder() |
| h(&router.Context{ |
| Request: (&http.Request{Form: f}).WithContext(ctx), |
| Writer: rr, |
| }) |
| expCT := "text/plain; charset=utf-8" |
| if ct == "json" { |
| expCT = "application/json; charset=utf-8" |
| } |
| So(rr.Header().Get("Content-Type"), ShouldEqual, expCT) |
| code = rr.Code |
| body = strings.TrimSpace(rr.Body.String()) |
| return |
| } |
| |
| Convey("handleLegacyInstance works", func() { |
| callInstance := func(pkg, iid, ct string) (code int, body string) { |
| return callHandler(adaptGrpcErr(impl.handleLegacyInstance), url.Values{ |
| "package_name": {pkg}, |
| "instance_id": {iid}, |
| }, ct) |
| } |
| |
| Convey("Happy path", func() { |
| code, body := callInstance("a/b", strings.Repeat("a", 40), "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "fetch_url": "http://fake/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", |
| "instance": { |
| "package_name": "a/b", |
| "instance_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", |
| "registered_by": "user:reg@example.com", |
| "registered_ts": "1454472306000000" |
| }, |
| "status": "SUCCESS" |
| }`) |
| }) |
| |
| Convey("Bad package", func() { |
| code, body := callInstance("///", strings.Repeat("a", 40), "plain") |
| So(code, ShouldEqual, http.StatusBadRequest) |
| So(body, ShouldContainSubstring, "invalid package name") |
| }) |
| |
| Convey("Bad instance ID", func() { |
| code, body := callInstance("a/b", strings.Repeat("a", 99), "plain") |
| So(code, ShouldEqual, http.StatusBadRequest) |
| So(body, ShouldContainSubstring, "not a valid package instance ID") |
| }) |
| |
| Convey("No access", func() { |
| code, body := callInstance("z/z/z", strings.Repeat("a", 40), "plain") |
| So(code, ShouldEqual, http.StatusForbidden) |
| So(body, ShouldContainSubstring, "not allowed to see") |
| }) |
| |
| Convey("Missing pkg", func() { |
| code, body := callInstance("a/z/z", strings.Repeat("a", 40), "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "error_message": "no such package: a/z/z", |
| "status": "INSTANCE_NOT_FOUND" |
| }`) |
| }) |
| }) |
| |
| Convey("handleLegacyResolve works", func() { |
| callResolve := func(pkg, ver, ct string) (code int, body string) { |
| return callHandler(adaptGrpcErr(impl.handleLegacyResolve), url.Values{ |
| "package_name": {pkg}, |
| "version": {ver}, |
| }, ct) |
| } |
| |
| Convey("Happy path", func() { |
| code, body := callResolve("a/b", strings.Repeat("a", 40), "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "instance_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", |
| "status": "SUCCESS" |
| }`) |
| }) |
| |
| Convey("Bad request", func() { |
| code, body := callResolve("///", strings.Repeat("a", 40), "plain") |
| So(code, ShouldEqual, http.StatusBadRequest) |
| So(body, ShouldContainSubstring, "invalid package name") |
| }) |
| |
| Convey("No access", func() { |
| code, body := callResolve("z/z/z", strings.Repeat("a", 40), "plain") |
| So(code, ShouldEqual, http.StatusForbidden) |
| So(body, ShouldContainSubstring, "not allowed to see") |
| }) |
| |
| Convey("Missing pkg", func() { |
| code, body := callResolve("a/z/z", strings.Repeat("a", 40), "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "error_message": "no such package: a/z/z", |
| "status": "INSTANCE_NOT_FOUND" |
| }`) |
| }) |
| |
| Convey("Ambiguous version", func() { |
| code, body := callResolve("a/b", "k:v", "json") |
| So(code, ShouldEqual, http.StatusOK) |
| So(body, ShouldEqual, `{ |
| "error_message": "ambiguity when resolving the tag, more than one instance has it", |
| "status": "AMBIGUOUS_VERSION" |
| }`) |
| }) |
| }) |
| }) |
| } |
| |
| func TestParseDownloadPath(t *testing.T) { |
| t.Parallel() |
| |
| Convey("OK", t, func() { |
| pkg, ver, err := parseDownloadPath("/a/b/c/+/latest") |
| So(err, ShouldBeNil) |
| So(pkg, ShouldEqual, "a/b/c") |
| So(ver, ShouldEqual, "latest") |
| |
| pkg, ver, err = parseDownloadPath("/a/b/c/+/repo:https://abc") |
| So(err, ShouldBeNil) |
| So(pkg, ShouldEqual, "a/b/c") |
| So(ver, ShouldEqual, "repo:https://abc") |
| |
| pkg, ver, err = parseDownloadPath("/a/b/c/+/" + strings.Repeat("a", 40)) |
| So(err, ShouldBeNil) |
| So(pkg, ShouldEqual, "a/b/c") |
| So(ver, ShouldEqual, strings.Repeat("a", 40)) |
| }) |
| |
| Convey("Bad chunks", t, func() { |
| _, _, err := parseDownloadPath("/+/latest") |
| So(err, ShouldErrLike, `should have form`) |
| |
| _, _, err = parseDownloadPath("latest") |
| So(err, ShouldErrLike, `should have form`) |
| |
| _, _, err = parseDownloadPath("/a/b/c") |
| So(err, ShouldErrLike, `should have form`) |
| }) |
| |
| Convey("Bad package", t, func() { |
| _, _, err := parseDownloadPath("BAD/+/latest") |
| So(err, ShouldErrLike, `invalid package name`) |
| |
| _, _, err = parseDownloadPath("/a//b/+/latest") |
| So(err, ShouldErrLike, `invalid package name`) |
| |
| _, _, err = parseDownloadPath("//+/latest") |
| So(err, ShouldErrLike, `invalid package name`) |
| }) |
| |
| Convey("Bad version", t, func() { |
| _, _, err := parseDownloadPath("a/+/") |
| So(err, ShouldErrLike, `bad version`) |
| |
| _, _, err = parseDownloadPath("a/+/!!!!") |
| So(err, ShouldErrLike, `bad version`) |
| }) |
| } |