From 3a3cdd7219e3c38164a8a6609144fecb42ed6947 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Thu, 20 Aug 2020 13:56:52 -0400 Subject: [PATCH] Static host set repository methods (#289) * Implement CreateSet * Implement UpdateSet * Implement LookupSet * Implement ListSets * Implement DeleteSet * Remove unused parameter from test helper function * Update to use new kms structure * Remove unused parameter for test helper function * Fix resource-type string for host set --- internal/host/static/host_catalog_test.go | 10 +- internal/host/static/host_set.go | 27 + internal/host/static/host_set_test.go | 2 +- internal/host/static/host_test.go | 2 +- internal/host/static/immutable_fields_test.go | 8 +- .../static/repository_host_catalog_test.go | 6 +- internal/host/static/repository_host_set.go | 222 +++++ .../host/static/repository_host_set_test.go | 849 ++++++++++++++++++ 8 files changed, 1112 insertions(+), 14 deletions(-) create mode 100644 internal/host/static/repository_host_set.go create mode 100644 internal/host/static/repository_host_set_test.go diff --git a/internal/host/static/host_catalog_test.go b/internal/host/static/host_catalog_test.go index d656b3cd9..f3bbc62bf 100644 --- a/internal/host/static/host_catalog_test.go +++ b/internal/host/static/host_catalog_test.go @@ -4,13 +4,13 @@ import ( "context" "testing" - "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/host/static/store" - "github.com/hashicorp/boundary/internal/iam" - wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/static/store" + "github.com/hashicorp/boundary/internal/iam" ) func TestHostCatalog_New(t *testing.T) { @@ -109,7 +109,7 @@ func TestHostCatalog_New(t *testing.T) { } } -func testCatalog(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, scopeId string) *HostCatalog { +func testCatalog(t *testing.T, conn *gorm.DB, scopeId string) *HostCatalog { t.Helper() cats := TestCatalogs(t, conn, scopeId, 1) return cats[0] diff --git a/internal/host/static/host_set.go b/internal/host/static/host_set.go index 9cdfbca93..67e43cfe0 100644 --- a/internal/host/static/host_set.go +++ b/internal/host/static/host_set.go @@ -5,6 +5,8 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/host/static/store" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" ) // A HostSet is a collection of hosts from the set's catalog. @@ -45,3 +47,28 @@ func (s *HostSet) TableName() string { func (s *HostSet) SetTableName(n string) { s.tableName = n } + +func allocHostSet() *HostSet { + return &HostSet{ + HostSet: &store.HostSet{}, + } +} + +func (s *HostSet) clone() *HostSet { + cp := proto.Clone(s.HostSet) + return &HostSet{ + HostSet: cp.(*store.HostSet), + } +} + +func (s *HostSet) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{s.PublicId}, + "resource-type": []string{"static-host-set"}, + "op-type": []string{op.String()}, + } + if s.CatalogId != "" { + metadata["catalog-id"] = []string{s.CatalogId} + } + return metadata +} diff --git a/internal/host/static/host_set_test.go b/internal/host/static/host_set_test.go index ee3fd2080..5ef60f6d3 100644 --- a/internal/host/static/host_set_test.go +++ b/internal/host/static/host_set_test.go @@ -16,7 +16,7 @@ func TestHostSet_New(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) conn.LogMode(false) type args struct { diff --git a/internal/host/static/host_test.go b/internal/host/static/host_test.go index 378975e50..1a8496ab6 100644 --- a/internal/host/static/host_test.go +++ b/internal/host/static/host_test.go @@ -15,7 +15,7 @@ func TestHost_New(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) conn.LogMode(false) type args struct { diff --git a/internal/host/static/immutable_fields_test.go b/internal/host/static/immutable_fields_test.go index 6dfe7f656..e87374e3d 100644 --- a/internal/host/static/immutable_fields_test.go +++ b/internal/host/static/immutable_fields_test.go @@ -21,7 +21,7 @@ func TestHostCatalog_ImmutableFields(t *testing.T) { wrapper := db.TestWrapper(t) ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} o, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - new := testCatalog(t, conn, wrapper, prj.PublicId) + new := testCatalog(t, conn, prj.PublicId) var tests = []struct { name string @@ -86,7 +86,7 @@ func TestStaticHost_ImmutableFields(t *testing.T) { ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) hosts := TestHosts(t, conn, cat.GetPublicId(), 1) new := hosts[0] @@ -161,7 +161,7 @@ func TestStaticHostSet_ImmutableFields(t *testing.T) { ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) sets := testSets(t, conn, cat.GetPublicId(), 1) new := sets[0] @@ -235,7 +235,7 @@ func TestStaticHostSetMember_ImmutableFields(t *testing.T) { wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) sets := testSets(t, conn, cat.GetPublicId(), 1) hosts := TestHosts(t, conn, cat.GetPublicId(), 1) diff --git a/internal/host/static/repository_host_catalog_test.go b/internal/host/static/repository_host_catalog_test.go index 43029abb2..74ab1122b 100644 --- a/internal/host/static/repository_host_catalog_test.go +++ b/internal/host/static/repository_host_catalog_test.go @@ -549,7 +549,7 @@ func TestRepository_UpdateCatalog(t *testing.T) { iamRepo := iam.TestRepo(t, conn, wrapper) _, prj1 := iam.TestScopes(t, iamRepo) _, prj2 := iam.TestScopes(t, iamRepo) - c1, c2 := testCatalog(t, conn, wrapper, prj1.PublicId), testCatalog(t, conn, wrapper, prj2.PublicId) + c1, c2 := testCatalog(t, conn, prj1.PublicId), testCatalog(t, conn, prj2.PublicId) assert.NotEqual(c1.ScopeId, c2.ScopeId) orig := c1.clone() @@ -570,7 +570,7 @@ func TestRepository_LookupCatalog(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) badId, err := newHostCatalogId() assert.NoError(t, err) assert.NotNil(t, badId) @@ -632,7 +632,7 @@ func TestRepository_DeleteCatalog(t *testing.T) { wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cat := testCatalog(t, conn, wrapper, prj.PublicId) + cat := testCatalog(t, conn, prj.PublicId) badId, err := newHostCatalogId() assert.NoError(t, err) assert.NotNil(t, badId) diff --git a/internal/host/static/repository_host_set.go b/internal/host/static/repository_host_set.go new file mode 100644 index 000000000..e208ea975 --- /dev/null +++ b/internal/host/static/repository_host_set.go @@ -0,0 +1,222 @@ +package static + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + dbcommon "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// CreateSet inserts s into the repository and returns a new HostSet +// containing the host set's PublicId. s is not changed. s must contain a +// valid CatalogId. s must not contain a PublicId. The PublicId is +// generated and assigned by this method. opt is ignored. +// +// Both s.Name and s.Description are optional. If s.Name is set, it must be +// unique within s.CatalogId. +func (r *Repository) CreateSet(ctx context.Context, scopeId string, s *HostSet, opt ...Option) (*HostSet, error) { + if s == nil { + return nil, fmt.Errorf("create: static host set: %w", db.ErrNilParameter) + } + if s.HostSet == nil { + return nil, fmt.Errorf("create: static host set: embedded HostSet: %w", db.ErrNilParameter) + } + if s.CatalogId == "" { + return nil, fmt.Errorf("create: static host set: no catalog id: %w", db.ErrInvalidParameter) + } + if s.PublicId != "" { + return nil, fmt.Errorf("create: static host set: public id not empty: %w", db.ErrInvalidParameter) + } + if scopeId == "" { + return nil, fmt.Errorf("create: static host set: no scopeId: %w", db.ErrNilParameter) + } + s = s.clone() + + id, err := newHostSetId() + if err != nil { + return nil, fmt.Errorf("create: static host set: %w", err) + } + s.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, fmt.Errorf("create: static host set: unable to get oplog wrapper: %w", err) + } + + var newHostSet *HostSet + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newHostSet = s.clone() + return w.Create(ctx, newHostSet, db.WithOplog(oplogWrapper, s.oplog(oplog.OpType_OP_TYPE_CREATE))) + }, + ) + + if err != nil { + if db.IsUniqueError(err) { + return nil, fmt.Errorf("create: static host set: in catalog: %s: name %s already exists: %w", + s.CatalogId, s.Name, db.ErrNotUnique) + } + return nil, fmt.Errorf("create: static host set: in catalog: %s: %w", s.CatalogId, err) + } + return newHostSet, nil +} + +// UpdateSet updates the repository entry for s.PublicId with the values in +// s for the fields listed in fieldMaskPaths. It returns a new HostSet +// containing the updated values and a count of the number of records +// updated. s is not changed. +// +// s must contain a valid PublicId. Only s.Name and s.Description can be +// updated. If s.Name is set to a non-empty string, it must be unique +// within s.CatalogId. +// +// An attribute of s will be set to NULL in the database if the attribute +// in s is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateSet(ctx context.Context, scopeId string, s *HostSet, version uint32, fieldMaskPaths []string, opt ...Option) (*HostSet, int, error) { + if s == nil { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: %w", db.ErrNilParameter) + } + if s.HostSet == nil { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: embedded HostSet: %w", db.ErrNilParameter) + } + if s.PublicId == "" { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: missing public id: %w", db.ErrInvalidParameter) + } + if version == 0 { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: no version supplied: %w", db.ErrInvalidParameter) + } + if scopeId == "" { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: no scopeId: %w", db.ErrNilParameter) + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold("Name", f): + case strings.EqualFold("Description", f): + default: + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: field: %s: %w", f, db.ErrInvalidFieldMask) + } + } + var dbMask, nullFields []string + dbMask, nullFields = dbcommon.BuildUpdatePaths( + map[string]interface{}{ + "Name": s.Name, + "Description": s.Description, + }, + fieldMaskPaths, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: %w", db.ErrEmptyFieldMask) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: unable to get oplog wrapper: %w", err) + } + + var rowsUpdated int + var returnedHostSet *HostSet + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedHostSet = s.clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedHostSet, dbMask, nullFields, + db.WithOplog(oplogWrapper, s.oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithVersion(&version)) + if err == nil && rowsUpdated > 1 { + return db.ErrMultipleRecords + } + return err + }, + ) + + if err != nil { + if db.IsUniqueError(err) { + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: %s: name %s already exists: %w", + s.PublicId, s.Name, db.ErrNotUnique) + } + return nil, db.NoRowsAffected, fmt.Errorf("update: static host set: %s: %w", s.PublicId, err) + } + + return returnedHostSet, rowsUpdated, nil +} + +// LookupSet will look up a host set in the repository. If the host set is +// not found, it will return nil, nil. All options are ignored. +func (r *Repository) LookupSet(ctx context.Context, publicId string, opt ...Option) (*HostSet, error) { + if publicId == "" { + return nil, fmt.Errorf("lookup: static host set: missing public id %w", db.ErrInvalidParameter) + } + s := allocHostSet() + s.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, s); err != nil { + if errors.Is(err, db.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("lookup: static host set: failed %w for %s", err, publicId) + } + return s, nil +} + +// ListSets returns a slice of HostSets for the catalogId. WithLimit is the +// only option supported. +func (r *Repository) ListSets(ctx context.Context, catalogId string, opt ...Option) ([]*HostSet, error) { + if catalogId == "" { + return nil, fmt.Errorf("list: static host set: missing catalog 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 sets []*HostSet + err := r.reader.SearchWhere(ctx, &sets, "catalog_id = ?", []interface{}{catalogId}, db.WithLimit(limit)) + if err != nil { + return nil, fmt.Errorf("list: static host set: %w", err) + } + return sets, nil +} + +// DeleteSet deletes the host set for the provided id from the repository +// returning a count of the number of records deleted. All options are +// ignored. +func (r *Repository) DeleteSet(ctx context.Context, scopeId string, publicId string, opt ...Option) (int, error) { + if publicId == "" { + return db.NoRowsAffected, fmt.Errorf("delete: static host set: missing public id: %w", db.ErrInvalidParameter) + } + if scopeId == "" { + return db.NoRowsAffected, fmt.Errorf("delete: static host set: no scopeId: %w", db.ErrNilParameter) + } + s := allocHostSet() + s.PublicId = publicId + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete: static host set: unable to get oplog wrapper: %w", err) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + ds := s.clone() + rowsDeleted, err = w.Delete(ctx, ds, db.WithOplog(oplogWrapper, s.oplog(oplog.OpType_OP_TYPE_DELETE))) + if err == nil && rowsDeleted > 1 { + return db.ErrMultipleRecords + } + return err + }, + ) + + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete: static host set: %s: %w", publicId, err) + } + + return rowsDeleted, nil +} diff --git a/internal/host/static/repository_host_set_test.go b/internal/host/static/repository_host_set_test.go new file mode 100644 index 000000000..fc0000f18 --- /dev/null +++ b/internal/host/static/repository_host_set_test.go @@ -0,0 +1,849 @@ +package static + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/host/static/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestRepository_CreateSet(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + + var tests = []struct { + name string + in *HostSet + opts []Option + want *HostSet + wantIsErr error + }{ + { + name: "nil-HostSet", + wantIsErr: db.ErrNilParameter, + }, + { + name: "nil-embedded-HostSet", + in: &HostSet{}, + wantIsErr: db.ErrNilParameter, + }, + { + name: "invalid-no-catalog-id", + in: &HostSet{ + HostSet: &store.HostSet{}, + }, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "invalid-public-id-set", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + PublicId: "abcd_OOOOOOOOOO", + }, + }, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid-no-options", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + }, + }, + }, + { + name: "valid-with-name", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Name: "test-name-repo", + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Name: "test-name-repo", + }, + }, + }, + { + name: "valid-with-description", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Description: ("test-description-repo"), + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Description: ("test-description-repo"), + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), tt.in, tt.opts...) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.Empty(tt.in.PublicId) + require.NotNil(got) + assertPublicId(t, HostSetPrefix, got.PublicId) + assert.NotSame(tt.in, got) + assert.Equal(tt.want.Name, got.Name) + assert.Equal(tt.want.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + + in := &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Name: "test-name-repo", + }, + } + + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, HostSetPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + assert.Truef(errors.Is(err, db.ErrNotUnique), "want err: %v got: %v", db.ErrNotUnique, err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-names-diff-catalogs", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalogs := TestCatalogs(t, conn, prj.PublicId, 2) + + catalogA, catalogB := catalogs[0], catalogs[1] + + in := &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + } + in2 := in.clone() + + in.CatalogId = catalogA.PublicId + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, HostSetPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.CatalogId = catalogB.PublicId + got2, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in2) + require.NoError(err) + require.NotNil(got2) + assertPublicId(t, HostSetPrefix, got2.PublicId) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) +} + +func TestRepository_UpdateSet(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + changeName := func(n string) func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + s.Name = n + return s + } + } + + changeDescription := func(d string) func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + s.Description = d + return s + } + } + + makeNil := func() func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + return nil + } + } + + makeEmbeddedNil := func() func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + return &HostSet{} + } + } + + deletePublicId := func() func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + s.PublicId = "" + return s + } + } + + nonExistentPublicId := func() func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + s.PublicId = "abcd_OOOOOOOOOO" + return s + } + } + + combine := func(fns ...func(s *HostSet) *HostSet) func(*HostSet) *HostSet { + return func(s *HostSet) *HostSet { + for _, fn := range fns { + s = fn(s) + } + return s + } + } + + var tests = []struct { + name string + orig *HostSet + chgFn func(*HostSet) *HostSet + masks []string + want *HostSet + wantCount int + wantIsErr error + }{ + { + name: "nil-host-set", + orig: &HostSet{ + HostSet: &store.HostSet{}, + }, + chgFn: makeNil(), + masks: []string{"Name", "Description"}, + wantIsErr: db.ErrNilParameter, + }, + { + name: "nil-embedded-host-set", + orig: &HostSet{ + HostSet: &store.HostSet{}, + }, + chgFn: makeEmbeddedNil(), + masks: []string{"Name", "Description"}, + wantIsErr: db.ErrNilParameter, + }, + { + name: "no-public-id", + orig: &HostSet{ + HostSet: &store.HostSet{}, + }, + chgFn: deletePublicId(), + masks: []string{"Name", "Description"}, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "updating-non-existent-host-set", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + }, + chgFn: combine(nonExistentPublicId(), changeName("test-update-name-repo")), + masks: []string{"Name"}, + wantIsErr: db.ErrRecordNotFound, + }, + { + name: "empty-field-mask", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + wantIsErr: db.ErrEmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "CatalogId"}, + wantIsErr: db.ErrInvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Bilbo"}, + wantIsErr: db.ErrInvalidFieldMask, + }, + { + name: "change-name", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Name"}, + want: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + orig: &HostSet{ + HostSet: &store.HostSet{ + Description: "test-description-repo", + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{"Description"}, + want: &HostSet{ + HostSet: &store.HostSet{ + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Description: "test-description-repo", + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{"Name", "Description"}, + want: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &HostSet{ + HostSet: &store.HostSet{ + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + orig: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &HostSet{ + HostSet: &store.HostSet{ + Name: "test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + + tt.orig.CatalogId = catalog.PublicId + orig, err := repo.CreateSet(context.Background(), prj.GetPublicId(), tt.orig) + assert.NoError(err) + require.NotNil(orig) + + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + got, gotCount, err := repo.UpdateSet(context.Background(), prj.GetPublicId(), orig, 1, tt.masks) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tt.orig.PublicId) + require.NotNil(got) + assertPublicId(t, HostSetPrefix, got.PublicId) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(tt.orig, got) + assert.Equal(tt.orig.CatalogId, got.CatalogId) + dbassert := dbassert.New(t, rw) + if tt.want.Name == "" { + dbassert.IsNull(got, "name") + return + } + assert.Equal(tt.want.Name, got.Name) + if tt.want.Description == "" { + dbassert.IsNull(got, "description") + return + } + assert.Equal(tt.want.Description, got.Description) + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + + name := "test-dup-name" + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + ss := testSets(t, conn, catalog.PublicId, 2) + + sA, sB := ss[0], ss[1] + + sA.Name = name + got1, gotCount1, err := repo.UpdateSet(context.Background(), prj.GetPublicId(), sA, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got1) + assert.Equal(name, got1.Name) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, sA.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + + sB.Name = name + got2, gotCount2, err := repo.UpdateSet(context.Background(), prj.GetPublicId(), sB, 1, []string{"name"}) + assert.Truef(errors.Is(err, db.ErrNotUnique), "want err: %v got: %v", db.ErrNotUnique, err) + assert.Nil(got2) + assert.Equal(db.NoRowsAffected, gotCount2, "row count") + err = db.TestVerifyOplog(t, rw, sB.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + + t.Run("valid-duplicate-names-diff-Catalogs", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalogs := TestCatalogs(t, conn, prj.PublicId, 2) + + catalogA, catalogB := catalogs[0], catalogs[1] + + in := &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + }, + } + in2 := in.clone() + + in.CatalogId = catalogA.PublicId + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(got) + assertPublicId(t, HostSetPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2.CatalogId = catalogB.PublicId + in2.Name = "first-name" + got2, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in2) + assert.NoError(err) + require.NotNil(got2) + got2.Name = got.Name + got3, gotCount3, err := repo.UpdateSet(context.Background(), prj.GetPublicId(), got2, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got3) + assert.NotSame(got2, got3) + assert.Equal(got.Name, got3.Name) + assert.Equal(got2.Description, got3.Description) + assert.Equal(1, gotCount3, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, got2.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) + + t.Run("change-scope-id", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalogs := TestCatalogs(t, conn, prj.PublicId, 2) + + catalogA, catalogB := catalogs[0], catalogs[1] + + sA := testSets(t, conn, catalogA.PublicId, 1)[0] + sB := testSets(t, conn, catalogB.PublicId, 1)[0] + + assert.NotEqual(sA.CatalogId, sB.CatalogId) + orig := sA.clone() + + sA.CatalogId = sB.CatalogId + assert.Equal(sA.CatalogId, sB.CatalogId) + + got1, gotCount1, err := repo.UpdateSet(context.Background(), prj.GetPublicId(), sA, 1, []string{"name"}) + + assert.NoError(err) + require.NotNil(got1) + assert.Equal(orig.CatalogId, got1.CatalogId) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, sA.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) +} + +func TestRepository_LookupSet(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + hostSet := testSets(t, conn, catalog.PublicId, 1)[0] + + hostSetId, err := newHostSetId() + require.NoError(t, err) + var tests = []struct { + name string + in string + want *HostSet + wantIsErr error + }{ + { + name: "with-no-public-id", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "with-non-existing-host-set-id", + in: hostSetId, + }, + { + name: "with-existing-host-set-id", + in: hostSet.PublicId, + want: hostSet, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.LookupSet(context.Background(), tt.in) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_ListSets(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iamRepo) + catalogs := TestCatalogs(t, conn, prj.PublicId, 2) + catalogA, catalogB := catalogs[0], catalogs[1] + + hostSets := testSets(t, conn, catalogA.PublicId, 3) + + var tests = []struct { + name string + in string + opts []Option + want []*HostSet + wantIsErr error + }{ + { + name: "with-no-catalog-id", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "Catalog-with-no-host-sets", + in: catalogB.PublicId, + want: []*HostSet{}, + }, + { + name: "Catalog-with-host-sets", + in: catalogA.PublicId, + want: hostSets, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListSets(context.Background(), tt.in, tt.opts...) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + opts := []cmp.Option{ + cmpopts.SortSlices(func(x, y *HostSet) bool { return x.PublicId < y.PublicId }), + protocmp.Transform(), + } + assert.Empty(cmp.Diff(tt.want, got, opts...)) + }) + } +} + +func TestRepository_ListSets_Limits(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + count := 10 + hostSets := testSets(t, conn, catalog.PublicId, count) + + var tests = []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "With no limits", + wantLen: count, + }, + { + name: "With repo limit", + repoOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative repo limit", + repoOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "With List limit", + listOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative List limit", + listOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "With repo smaller than list limit", + repoOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(6)}, + wantLen: 6, + }, + { + name: "With repo larger than list limit", + repoOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(2)}, + wantLen: 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms, tt.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListSets(context.Background(), hostSets[0].CatalogId, tt.listOpts...) + require.NoError(err) + assert.Len(got, tt.wantLen) + }) + } +} + +func TestRepository_DeleteSet(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0] + hostSet := testSets(t, conn, catalog.PublicId, 1)[0] + + newHostSetId, err := newHostSetId() + require.NoError(t, err) + var tests = []struct { + name string + in string + want int + wantIsErr error + }{ + { + name: "With no public id", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "With non existing host set id", + in: newHostSetId, + want: 0, + }, + { + name: "With existing host set id", + in: hostSet.PublicId, + want: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.DeleteSet(context.Background(), prj.PublicId, tt.in) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err) + assert.Zero(got) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +}