diff --git a/api/hosts/host_catalog_test.go b/api/hosts/host_catalog_test.go index 52436a059..7ee6e66c6 100644 --- a/api/hosts/host_catalog_test.go +++ b/api/hosts/host_catalog_test.go @@ -1,6 +1,7 @@ package hosts_test import ( + "fmt" "net/http" "testing" @@ -13,6 +14,67 @@ import ( "github.com/stretchr/testify/require" ) +func TestCatalog_List(t *testing.T) { + assert, require := assert.New(t), require.New(t) + amId := "paum_1234567890" + tc := controller.NewTestController(t, &controller.TestControllerOpts{ + DisableAuthorizationFailures: true, + DefaultAuthMethodId: amId, + DefaultLoginName: "user", + DefaultPassword: "passpass", + }) + defer tc.Shutdown() + + client := tc.Client() + _, proj := iam.TestScopes(t, tc.IamRepo()) + client.SetScopeId(proj.GetPublicId()) + catalogClient := hosts.NewHostCatalogsClient(client) + + ul, apiErr, err := catalogClient.List(tc.Context()) + assert.NoError(err) + assert.Nil(apiErr) + assert.Empty(ul) + + var expected []*hosts.HostCatalog + for i := 0; i < 10; i++ { + expected = append(expected, &hosts.HostCatalog{Name: fmt.Sprint(i)}) + } + + expected[0], apiErr, err = catalogClient.Create(tc.Context(), "static", hosts.WithName(expected[0].Name)) + assert.NoError(err) + assert.Nil(apiErr) + + ul, apiErr, err = catalogClient.List(tc.Context()) + assert.NoError(err) + assert.Nil(apiErr) + assert.ElementsMatch(comparableSlice(expected[:1]), comparableSlice(ul)) + + for i := 1; i < 10; i++ { + expected[i], apiErr, err = catalogClient.Create(tc.Context(), "static", hosts.WithName(expected[i].Name)) + assert.NoError(err) + assert.Nil(apiErr) + } + ul, apiErr, err = catalogClient.List(tc.Context()) + require.NoError(err) + assert.Nil(apiErr) + assert.ElementsMatch(comparableSlice(expected), comparableSlice(ul)) +} + +func comparableSlice(in []*hosts.HostCatalog) []hosts.HostCatalog { + var filtered []hosts.HostCatalog + for _, i := range in { + p := hosts.HostCatalog{ + Id: i.Id, + Name: i.Name, + Description: i.Description, + CreatedTime: i.CreatedTime, + UpdatedTime: i.UpdatedTime, + } + filtered = append(filtered, p) + } + return filtered +} + func TestCatalogs_Crud(t *testing.T) { assert, require := assert.New(t), require.New(t) amId := "paum_1234567890" diff --git a/internal/host/static/repository_host_catalog.go b/internal/host/static/repository_host_catalog.go index 3655f1109..e2068a07b 100644 --- a/internal/host/static/repository_host_catalog.go +++ b/internal/host/static/repository_host_catalog.go @@ -179,6 +179,25 @@ func (r *Repository) LookupCatalog(ctx context.Context, id string, opt ...Option return c, nil } +// ListCatalogs returns a slice of HostCatalogs for the scopeId. WithLimit is the only option supported. +func (r *Repository) ListCatalogs(ctx context.Context, scopeId string, opt ...Option) ([]*HostCatalog, error) { + if scopeId == "" { + return nil, fmt.Errorf("list: static host catalog: missing scope id: %w", db.ErrInvalidParameter) + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var hostCatalogs []*HostCatalog + err := r.reader.SearchWhere(ctx, &hostCatalogs, "scope_id = ?", []interface{}{scopeId}, db.WithLimit(limit)) + if err != nil { + return nil, fmt.Errorf("list: static host catalog: %w", err) + } + return hostCatalogs, nil +} + // DeleteCatalog deletes id from the repository returning a count of the // number of records deleted. func (r *Repository) DeleteCatalog(ctx context.Context, id string, opt ...Option) (int, error) { diff --git a/internal/host/static/testing.go b/internal/host/static/testing.go new file mode 100644 index 000000000..e452dc5dc --- /dev/null +++ b/internal/host/static/testing.go @@ -0,0 +1,34 @@ +package static + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" +) + +// TestCatalogs creates count number of static host catalogs to the provided DB +// with the provided scope id. If any errors are encountered during the creation of +// the host catalog, the test will fail. +func TestCatalogs(t *testing.T, conn *gorm.DB, scopeId string, count int) []*HostCatalog { + t.Helper() + assert := assert.New(t) + var cats []*HostCatalog + for i := 0; i < count; i++ { + cat, err := NewHostCatalog(scopeId) + assert.NoError(err) + assert.NotNil(cat) + id, err := newHostCatalogId() + assert.NoError(err) + assert.NotEmpty(id) + cat.PublicId = id + + w := db.New(conn) + err2 := w.Create(context.Background(), cat) + assert.NoError(err2) + cats = append(cats, cat) + } + return cats +} diff --git a/internal/host/static/testing_test.go b/internal/host/static/testing_test.go new file mode 100644 index 000000000..1d72d3208 --- /dev/null +++ b/internal/host/static/testing_test.go @@ -0,0 +1,26 @@ +package static + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_TestCatalogs(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NotNil(proj) + assert.NotEmpty(proj.GetPublicId()) + + count := 4 + cs := TestCatalogs(t, conn, proj.GetPublicId(), count) + assert.Len(cs, count) + for _, c := range cs { + assert.NotEmpty(c.GetPublicId()) + } +} diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go index 38f74529c..7800a0793 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go @@ -47,7 +47,21 @@ func NewService(repoFn common.StaticRepoFactory) (Service, error) { } func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogsRequest) (*pbs.ListHostCatalogsResponse, error) { - return nil, status.Error(codes.Unimplemented, "Requested method is unimplemented for Host Catalogs.") + authResults := auth.Verify(ctx) + if authResults.Error != nil { + return nil, authResults.Error + } + if err := validateListRequest(req); err != nil { + return nil, err + } + ul, err := s.listFromRepo(ctx, authResults.Scope.GetId()) + if err != nil { + return nil, err + } + for _, item := range ul { + item.Scope = authResults.Scope + } + return &pbs.ListHostCatalogsResponse{Items: ul}, nil } // GetHostCatalog implements the interface pbs.HostCatalogServiceServer. @@ -135,6 +149,22 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*pb.HostCatalog, e return toProto(hc), nil } +func (s Service) listFromRepo(ctx context.Context, scopeId string) ([]*pb.HostCatalog, error) { + repo, err := s.staticRepoFn() + if err != nil { + return nil, err + } + ul, err := repo.ListCatalogs(ctx, scopeId) + if err != nil { + return nil, err + } + var outUl []*pb.HostCatalog + for _, u := range ul { + outUl = append(outUl, toProto(u)) + } + return outUl, nil +} + func (s Service) createInRepo(ctx context.Context, projId string, item *pb.HostCatalog) (*pb.HostCatalog, error) { var opts []static.Option if item.GetName() != nil { @@ -240,6 +270,14 @@ func validateGetRequest(req *pbs.GetHostCatalogRequest) error { return nil } +func validateListRequest(req *pbs.ListHostCatalogsRequest) error { + badFields := map[string]string{} + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields) + } + return nil +} + func validateCreateRequest(req *pbs.CreateHostCatalogRequest) error { badFields := map[string]string{} item := req.GetItem() diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go index 6505707c7..ab1fa197f 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/db" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/hosts" - "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" + scopepb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -62,7 +62,7 @@ func TestGet(t *testing.T) { pHostCatalog := &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: hc.GetName()}, Description: &wrappers.StringValue{Value: hc.GetDescription()}, CreatedTime: hc.CreateTime.GetTimestamp(), @@ -121,6 +121,89 @@ func TestGet(t *testing.T) { } } +func TestList(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, pNoCatalogs := iam.TestScopes(t, iamRepo) + _, pWithCatalogs := iam.TestScopes(t, iamRepo) + _, pWithOtherCatalogs := iam.TestScopes(t, iamRepo) + + var wantSomeCatalogs []*pb.HostCatalog + for _, hc := range static.TestCatalogs(t, conn, pWithCatalogs.GetPublicId(), 3) { + wantSomeCatalogs = append(wantSomeCatalogs, &pb.HostCatalog{ + Id: hc.GetPublicId(), + CreatedTime: hc.GetCreateTime().GetTimestamp(), + UpdatedTime: hc.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: pWithCatalogs.GetPublicId(), Type: scope.Project.String()}, + Version: 1, + Type: "static", + }) + } + + var wantOtherCatalogs []*pb.HostCatalog + for _, hc := range static.TestCatalogs(t, conn, pWithOtherCatalogs.GetPublicId(), 3) { + wantOtherCatalogs = append(wantOtherCatalogs, &pb.HostCatalog{ + Id: hc.GetPublicId(), + CreatedTime: hc.GetCreateTime().GetTimestamp(), + UpdatedTime: hc.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: pWithOtherCatalogs.GetPublicId(), Type: scope.Project.String()}, + Version: 1, + Type: "static", + }) + } + + cases := []struct { + name string + scopeId string + res *pbs.ListHostCatalogsResponse + errCode codes.Code + }{ + { + name: "List Some Catalogs", + scopeId: pWithCatalogs.GetPublicId(), + res: &pbs.ListHostCatalogsResponse{Items: wantSomeCatalogs}, + errCode: codes.OK, + }, + { + name: "List Other Catalogs", + scopeId: pWithOtherCatalogs.GetPublicId(), + res: &pbs.ListHostCatalogsResponse{Items: wantOtherCatalogs}, + errCode: codes.OK, + }, + { + name: "List No Catalogs", + scopeId: pNoCatalogs.GetPublicId(), + res: &pbs.ListHostCatalogsResponse{}, + errCode: codes.OK, + }, + // TODO: When an auth method doesn't exist, we should return a 404 instead of an empty list. + { + name: "Unfound Catalogs", + scopeId: scope.Project.Prefix() + "_DoesntExis", + res: &pbs.ListHostCatalogsResponse{}, + errCode: codes.OK, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := host_catalogs.NewService(repoFn) + require.NoError(err, "Couldn't create new auth_method service.") + + got, gErr := s.ListHostCatalogs(auth.DisabledAuthTestContext(auth.WithScopeId(tc.scopeId)), &pbs.ListHostCatalogsRequest{}) + assert.Equal(tc.errCode, status.Code(gErr), "ListHostCatalogs() for scope %q got error %v, wanted %v", tc.scopeId, gErr, tc.errCode) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostCatalogs() for scope %q got response %q, wanted %q", tc.scopeId, got, tc.res) + }) + } +} + func TestDelete(t *testing.T) { t.Parallel() require := require.New(t) @@ -244,7 +327,7 @@ func TestCreate(t *testing.T) { res: &pbs.CreateHostCatalogResponse{ Uri: fmt.Sprintf("scopes/%s/host-catalogs/%s_", proj.GetPublicId(), static.HostCatalogPrefix), Item: &pb.HostCatalog{ - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "name"}, Description: &wrappers.StringValue{Value: "desc"}, Type: "static", @@ -373,7 +456,7 @@ func TestUpdate(t *testing.T) { res: &pbs.UpdateHostCatalogResponse{ Item: &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "new"}, Description: &wrappers.StringValue{Value: "desc"}, CreatedTime: hc.GetCreateTime().GetTimestamp(), @@ -396,7 +479,7 @@ func TestUpdate(t *testing.T) { res: &pbs.UpdateHostCatalogResponse{ Item: &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "new"}, Description: &wrappers.StringValue{Value: "desc"}, CreatedTime: hc.GetCreateTime().GetTimestamp(), @@ -450,7 +533,7 @@ func TestUpdate(t *testing.T) { res: &pbs.UpdateHostCatalogResponse{ Item: &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Description: &wrappers.StringValue{Value: "default"}, CreatedTime: hc.GetCreateTime().GetTimestamp(), Type: "static", @@ -471,7 +554,7 @@ func TestUpdate(t *testing.T) { res: &pbs.UpdateHostCatalogResponse{ Item: &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "default"}, CreatedTime: hc.GetCreateTime().GetTimestamp(), Type: "static", @@ -493,7 +576,7 @@ func TestUpdate(t *testing.T) { res: &pbs.UpdateHostCatalogResponse{ Item: &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "updated"}, Description: &wrappers.StringValue{Value: "default"}, CreatedTime: hc.GetCreateTime().GetTimestamp(), @@ -516,7 +599,7 @@ func TestUpdate(t *testing.T) { res: &pbs.UpdateHostCatalogResponse{ Item: &pb.HostCatalog{ Id: hc.GetPublicId(), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "default"}, Description: &wrappers.StringValue{Value: "notignored"}, CreatedTime: hc.GetCreateTime().GetTimestamp(), @@ -536,7 +619,7 @@ func TestUpdate(t *testing.T) { }, Item: &pb.HostCatalog{ Name: &wrappers.StringValue{Value: "new"}, - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Description: &wrappers.StringValue{Value: "desc"}, }, }, @@ -551,7 +634,7 @@ func TestUpdate(t *testing.T) { }, Item: &pb.HostCatalog{ Id: "p_somethinge", - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, + Scope: &scopepb.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String()}, Name: &wrappers.StringValue{Value: "new"}, Description: &wrappers.StringValue{Value: "new desc"}, }},