You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/auth/ldap/repository_account_test.go

1344 lines
41 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/db"
dbassert "github.com/hashicorp/boundary/internal/db/assert"
"github.com/hashicorp/boundary/internal/errors"
"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_CreateAccount(t *testing.T) {
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
testCtx := context.Background()
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(t, err)
authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"})
tests := []struct {
name string
repo *Repository
in *Account
opts []Option
want *Account
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-account",
repo: testRepo,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing account: parameter violation: error #100",
},
{
name: "missing-embedded-account",
in: &Account{},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing embedded account: parameter violation: error #100",
},
{
name: "missing-auth-method-id",
in: &Account{
Account: &store.Account{
ScopeId: org.PublicId,
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing auth method id: parameter violation: error #100",
},
{
name: "missing-scope-id",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing scope id: parameter violation: error #100",
},
{
name: "invalid-public-id-set",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
PublicId: "invalid-public-id-set",
LoginName: "invalid-public-id-set",
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "public id must be empty: parameter violation: error #100",
},
{
name: "missing-login-name",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing login name: parameter violation: error #100",
},
{
name: "valid-no-options",
repo: testRepo,
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "valid-no-options",
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "valid-no-options",
},
},
},
{
name: "valid-with-name",
repo: testRepo,
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "valid-with-name",
Name: "test-name-repo",
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "valid-with-name",
Name: "test-name-repo",
},
},
},
{
name: "valid-with-description",
repo: testRepo,
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "valid-with-description",
Description: ("test-description-repo"),
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "valid-with-description",
Description: ("test-description-repo"),
},
},
},
{
name: "get-wrapper-err",
repo: func() *Repository {
kms := &mockGetWrapperer{
getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"),
}
r, err := NewRepository(testCtx, testRw, testRw, kms)
require.NoError(t, err)
return r
}(),
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "get-wrapper-err",
},
},
wantErrMatch: errors.T(errors.Encrypt),
wantErrContains: "get-db-wrapper-err: encryption issue",
},
{
name: "write-err",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket
mock.ExpectQuery(`INSERT`).WillReturnError(fmt.Errorf("write-err"))
mock.ExpectRollback()
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
ScopeId: org.PublicId,
LoginName: "get-wrapper-err",
},
},
wantErrMatch: errors.T(errors.Unknown),
wantErrContains: "write-err",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := tc.repo.CreateAccount(testCtx, tc.in, tc.opts...)
if tc.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err)
if tc.wantErrContains != "" {
assert.Contains(err.Error(), tc.wantErrContains)
}
return
}
require.NoError(err)
require.NotNil(got)
assertPublicId(t, globals.LdapAccountPrefix, got.PublicId)
assert.NotSame(tc.in, got)
assert.NotEmpty(got.CreateTime)
assert.NotEmpty(got.UpdateTime)
assert.Equal(got.CreateTime, got.UpdateTime)
tc.want.CreateTime = got.CreateTime
tc.want.UpdateTime = got.UpdateTime
tc.want.PublicId = got.PublicId
tc.want.Version = 1
assert.Empty(cmp.Diff(tc.want, got, protocmp.Transform()))
assert.NoError(db.TestVerifyOplog(t, testRw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
})
}
}
func TestRepository_CreateAccount_DuplicateFields(t *testing.T) {
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
testCtx := context.Background()
t.Run("invalid-duplicate-names", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"})
in := &Account{
Account: &store.Account{
AuthMethodId: authMethod.GetPublicId(),
ScopeId: org.GetPublicId(),
Name: "test-name-repo",
LoginName: "login-name",
},
}
got, err := repo.CreateAccount(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, globals.LdapAccountPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
in.PublicId = ""
got2, err := repo.CreateAccount(context.Background(), in)
assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err)
assert.Nil(got2)
})
t.Run("valid-duplicate-names-diff-parents", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
authMethodA := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"})
authMethodB := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap2"})
in := &Account{
Account: &store.Account{
ScopeId: org.PublicId,
Name: "test-name-repo",
LoginName: "login1",
},
}
in2 := in.clone()
in.AuthMethodId = authMethodA.GetPublicId()
got, err := repo.CreateAccount(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, globals.LdapAccountPrefix, 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.AuthMethodId = authMethodB.GetPublicId()
got2, err := repo.CreateAccount(context.Background(), in2)
assert.NoError(err)
require.NotNil(got2)
assertPublicId(t, globals.LdapAccountPrefix, got2.PublicId)
assert.NotSame(in2, got2)
assert.Equal(in2.Name, got2.Name)
assert.Equal(in2.Description, got2.Description)
assert.Equal(got2.CreateTime, got2.UpdateTime)
})
t.Run("invalid-duplicate-login-names", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"})
in := &Account{
Account: &store.Account{
AuthMethodId: authMethod.GetPublicId(),
ScopeId: org.PublicId,
LoginName: "login-name-1",
},
}
got, err := repo.CreateAccount(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, globals.LdapAccountPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
in.PublicId = ""
got2, err := repo.CreateAccount(context.Background(), in)
assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err)
assert.Nil(got2)
})
t.Run("valid-duplicate-login-name-diff-auth-method", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
authMethodA := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"})
authMethodB := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap2"})
in := &Account{
Account: &store.Account{
AuthMethodId: authMethodA.PublicId,
ScopeId: org.PublicId,
LoginName: "login-name-1",
},
}
in2 := in.clone()
in.AuthMethodId = authMethodA.GetPublicId()
got, err := repo.CreateAccount(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, globals.LdapAccountPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(in.LoginName, got.LoginName)
assert.Equal(got.CreateTime, got.UpdateTime)
in2.AuthMethodId = authMethodB.GetPublicId()
got2, err := repo.CreateAccount(context.Background(), in2)
assert.NoError(err)
require.NotNil(got2)
assertPublicId(t, globals.LdapAccountPrefix, got2.PublicId)
assert.NotSame(in2, got2)
assert.Equal(in2.Name, got2.Name)
assert.Equal(in2.Description, got2.Description)
assert.Equal(in2.LoginName, got2.LoginName)
assert.Equal(got2.CreateTime, got2.UpdateTime)
})
}
func TestRepository_LookupAccount(t *testing.T) {
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
testCtx := context.Background()
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(t, err)
authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
account := TestAccount(t, testConn, authMethod, "test-login-name")
newAcctId, err := newAccountId(testCtx, authMethod.PublicId, "test-not-matching")
require.NoError(t, err)
tests := []struct {
name string
repo *Repository
publicId string
want *Account
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-public-id",
repo: testRepo,
wantErrMatch: errors.T(errors.InvalidPublicId),
wantErrContains: "missing public id: parameter violation: error #102",
},
{
name: "not-found",
repo: testRepo,
publicId: newAcctId,
},
{
name: "read-err",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("read-err"))
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
publicId: newAcctId,
wantErrMatch: errors.T(errors.Unknown),
wantErrContains: "read-err",
},
{
name: "with-existing-account-id",
repo: testRepo,
publicId: account.GetPublicId(),
want: account,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := tc.repo.LookupAccount(testCtx, tc.publicId)
if tc.wantErrMatch != nil {
assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err)
if tc.wantErrContains != "" {
assert.Contains(err.Error(), tc.wantErrContains)
}
return
}
require.NoError(err)
assert.EqualValues(tc.want, got)
})
}
}
func TestRepository_DeleteAccount(t *testing.T) {
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testCtx := context.Background()
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
account := TestAccount(t, testConn, authMethod, "create-success")
newAcctId, err := newAccountId(testCtx, authMethod.PublicId, "not-matching")
require.NoError(t, err)
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(t, err)
require.NotNil(t, testRepo)
tests := []struct {
name string
repo *Repository
publicId string
want int
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-public-id",
repo: testRepo,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing public id: parameter violation: error #100",
},
{
name: "not-found",
repo: testRepo,
publicId: newAcctId,
wantErrMatch: errors.T(errors.RecordNotFound),
wantErrContains: "account not found",
},
{
name: "get-wrapper-err",
repo: func() *Repository {
kms := &mockGetWrapperer{
getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"),
}
r, err := NewRepository(testCtx, testRw, testRw, kms)
require.NoError(t, err)
return r
}(),
publicId: account.GetPublicId(),
wantErrMatch: errors.T(errors.Encrypt),
wantErrContains: "unable to get oplog wrapper",
},
{
name: "oplog-metadata-err",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id"}).AddRow("1", "global")) // get account without auth method id
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
publicId: account.GetPublicId(),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "ldap.(Account).oplog: missing auth method id",
},
{
name: "write-err",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("write-err")) // oplog: get ticket
mock.ExpectRollback()
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
publicId: account.GetPublicId(),
wantErrMatch: errors.T(errors.Unknown),
wantErrContains: "write-err",
},
{
name: "too-many-rows-affected",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket
mock.ExpectExec(`DELETE`).WillReturnResult(sqlmock.NewResult(1, 2))
mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: entry
mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: metadata
mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1)) // oplog: update ticket version
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket
mock.ExpectRollback()
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
publicId: account.GetPublicId(),
wantErrMatch: errors.T(errors.Unknown),
wantErrContains: "more than 1 resource would have been deleted",
},
{
name: "delete-err",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket
mock.ExpectExec(`DELETE`).WillReturnError(fmt.Errorf("delete-err"))
mock.ExpectRollback()
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
publicId: account.GetPublicId(),
wantErrMatch: errors.T(errors.Unknown),
wantErrContains: "delete-err",
},
{
name: "success",
repo: testRepo,
publicId: account.GetPublicId(),
want: 1,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := tc.repo.DeleteAccount(context.Background(), tc.publicId)
if tc.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err)
if tc.wantErrContains != "" {
assert.Contains(err.Error(), tc.wantErrContains)
}
return
}
require.NoError(err)
assert.EqualValues(tc.want, got)
})
}
}
func TestRepository_ListAccounts(t *testing.T) {
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testCtx := context.Background()
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(t, err)
authMethod1 := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
authMethod2 := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"})
authMethod3 := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"})
accounts1 := []*Account{
TestAccount(t, testConn, authMethod1, "create-success"),
TestAccount(t, testConn, authMethod1, "create-success2"),
TestAccount(t, testConn, authMethod1, "create-success3"),
}
accounts2 := []*Account{
TestAccount(t, testConn, authMethod2, "create-success"),
TestAccount(t, testConn, authMethod2, "create-success2"),
TestAccount(t, testConn, authMethod2, "create-success3"),
}
_ = accounts2
tests := []struct {
name string
repo *Repository
publicId string
opts []Option
want []*Account
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-auth-method-id",
repo: testRepo,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing auth method id: parameter violation: error #100",
},
{
name: "with-no-account-ids",
repo: testRepo,
publicId: authMethod3.GetPublicId(),
want: []*Account{},
},
{
name: "with-first-auth-method-id",
repo: testRepo,
publicId: authMethod1.GetPublicId(),
want: accounts1,
},
{
name: "read-err",
repo: func() *Repository {
conn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("read-err"))
rw := db.New(conn)
r, err := NewRepository(testCtx, rw, rw, testKms)
require.NoError(t, err)
return r
}(),
publicId: authMethod1.GetPublicId(),
wantErrMatch: errors.T(errors.Unknown),
wantErrContains: "read-err",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := tc.repo.ListAccounts(testCtx, tc.publicId, tc.opts...)
if tc.wantErrMatch != nil {
assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err)
if tc.wantErrContains != "" {
assert.Contains(err.Error(), tc.wantErrContains)
}
return
}
require.NoError(err)
sort.Slice(got, func(i, j int) bool {
return strings.Compare(got[i].LoginName, got[j].LoginName) < 0
})
assert.EqualValues(tc.want, got)
})
}
}
func TestRepository_ListAccounts_Limits(t *testing.T) {
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testCtx := context.Background()
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
accountCount := 10
for i := 0; i < accountCount; i++ {
TestAccount(t, testConn, am, fmt.Sprintf("create-success-%d", i))
}
tests := []struct {
name string
repoOpts []Option
listOpts []Option
wantLen int
}{
{
name: "with-no-limits",
wantLen: accountCount,
},
{
name: "with-repo-limit",
repoOpts: []Option{WithLimit(testCtx, 3)},
wantLen: 3,
},
{
name: "with-negative-repo-limit",
repoOpts: []Option{WithLimit(testCtx, -1)},
wantLen: accountCount,
},
{
name: "with-list-limit",
listOpts: []Option{WithLimit(testCtx, 3)},
wantLen: 3,
},
{
name: "with-negative-list-limit",
listOpts: []Option{WithLimit(testCtx, -1)},
wantLen: accountCount,
},
{
name: "with-repo-smaller-than-list-limit",
repoOpts: []Option{WithLimit(testCtx, 2)},
listOpts: []Option{WithLimit(testCtx, 6)},
wantLen: 6,
},
{
name: "with-repo-larger-than-list-limit",
repoOpts: []Option{WithLimit(testCtx, 6)},
listOpts: []Option{WithLimit(testCtx, 2)},
wantLen: 2,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms, tc.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListAccounts(context.Background(), am.GetPublicId(), tc.listOpts...)
require.NoError(err)
assert.Len(got, tc.wantLen)
})
}
}
func TestRepository_UpdateAccount(t *testing.T) {
testCtx := context.Background()
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(t, err)
require.NotNil(t, testRepo)
changeName := func(s string) func(*Account) *Account {
return func(a *Account) *Account {
a.Name = s
return a
}
}
changeDescription := func(s string) func(*Account) *Account {
return func(a *Account) *Account {
a.Description = s
return a
}
}
makeNil := func() func(*Account) *Account {
return func(a *Account) *Account {
return nil
}
}
makeEmbeddedNil := func() func(*Account) *Account {
return func(a *Account) *Account {
return &Account{}
}
}
deletePublicId := func() func(*Account) *Account {
return func(a *Account) *Account {
a.PublicId = ""
return a
}
}
nonExistentPublicId := func() func(*Account) *Account {
return func(a *Account) *Account {
a.PublicId = "abcd_OOOOOOOOOO"
return a
}
}
combine := func(fns ...func(a *Account) *Account) func(*Account) *Account {
return func(a *Account) *Account {
for _, fn := range fns {
a = fn(a)
}
return a
}
}
tests := []struct {
name string
repo *Repository
scopeId string
version uint32
orig *Account
chgFn func(*Account) *Account
masks []string
want *Account
wantCount int
wantIsErr errors.Code
wantErrContains string
}{
{
name: "nil-account",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{},
},
chgFn: makeNil(),
masks: []string{NameField, DescriptionField},
wantIsErr: errors.InvalidParameter,
wantErrContains: "missing Account: parameter violation: error #100",
},
{
name: "nil-embedded-account",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{},
},
chgFn: makeEmbeddedNil(),
masks: []string{NameField, DescriptionField},
wantIsErr: errors.InvalidParameter,
wantErrContains: "missing embedded Account: parameter violation: error #100",
},
{
name: "no-scope-id",
repo: testRepo,
version: 1,
orig: &Account{
Account: &store.Account{
Name: "no-scope-id-test-name-repo",
},
},
chgFn: changeName("no-scope-id-test-update-name-repo"),
masks: []string{NameField},
wantIsErr: errors.InvalidParameter,
wantErrContains: "missing scope id: parameter violation: error #100",
},
{
name: "missing-version",
repo: testRepo,
scopeId: org.GetPublicId(),
orig: &Account{
Account: &store.Account{
Name: "missing-version-test-name-repo",
},
},
chgFn: changeName("test-update-name-repo"),
masks: []string{NameField},
wantIsErr: errors.InvalidParameter,
wantErrContains: "missing version: parameter violation: error #100",
},
{
name: "no-public-id",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{},
},
chgFn: deletePublicId(),
masks: []string{NameField, DescriptionField},
wantIsErr: errors.InvalidPublicId,
wantErrContains: "missing public id: parameter violation: error #102",
},
{
name: "updating-non-existent-account",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "updating-non-existent-Account-test-name-repo",
},
},
chgFn: combine(nonExistentPublicId(), changeName("updating-non-existent-Account-test-update-name-repo")),
masks: []string{NameField},
wantIsErr: errors.RecordNotFound,
wantErrContains: "record not found",
},
{
name: "empty-field-mask",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "empty-field-mask-test-name-repo",
},
},
chgFn: changeName("empty-field-mask-test-update-name-repo"),
wantIsErr: errors.EmptyFieldMask,
wantErrContains: "missing field mask: parameter violation: error #104",
},
{
name: "read-only-fields-in-field-mask",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "read-only-fields-in-field-mask-test-name-repo",
},
},
chgFn: changeName("read-only-fields-in-field-mask-test-update-name-repo"),
masks: []string{"PublicId", "CreateTime", "UpdateTime", "AuthMethodId"},
wantIsErr: errors.InvalidFieldMask,
wantErrContains: "PublicId: parameter violation: error #103",
},
{
name: "unknown-field-in-field-mask",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "unknown-field-in-field-mask-test-name-repo",
},
},
chgFn: changeName("unknown-field-in-field-mask-test-update-name-repo"),
masks: []string{"Bilbo"},
wantIsErr: errors.InvalidFieldMask,
wantErrContains: "Bilbo: parameter violation: error #103",
},
{
name: "change-name",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "change-name-test-name-repo",
},
},
chgFn: changeName("change-name-test-update-name-repo"),
masks: []string{NameField},
want: &Account{
Account: &store.Account{
Name: "change-name-test-update-name-repo",
},
},
wantCount: 1,
},
{
name: "change-description",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Description: "test-description-repo",
},
},
chgFn: changeDescription("test-update-description-repo"),
masks: []string{DescriptionField},
want: &Account{
Account: &store.Account{
Description: "test-update-description-repo",
},
},
wantCount: 1,
},
{
name: "change-name-and-description",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "change-name-and-description-test-name-repo",
Description: "test-description-repo",
},
},
chgFn: combine(changeDescription("test-update-description-repo"), changeName("change-name-and-description-test-update-name-repo")),
masks: []string{NameField, DescriptionField},
want: &Account{
Account: &store.Account{
Name: "change-name-and-description-test-update-name-repo",
Description: "test-update-description-repo",
},
},
wantCount: 1,
},
{
name: "delete-name",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "delete-name-test-name-repo",
Description: "test-description-repo",
},
},
masks: []string{NameField},
chgFn: combine(changeDescription("test-update-description-repo"), changeName("")),
want: &Account{
Account: &store.Account{
Description: "test-description-repo",
},
},
wantCount: 1,
},
{
name: "delete-description",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "delete-description-test-name-repo",
Description: "test-description-repo",
},
},
masks: []string{DescriptionField},
chgFn: combine(changeDescription(""), changeName("test-update-name-repo")),
want: &Account{
Account: &store.Account{
Name: "delete-description-test-name-repo",
},
},
wantCount: 1,
},
{
name: "do-not-delete-name",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "do-not-delete-name-test-name-repo",
Description: "test-description-repo",
},
},
masks: []string{DescriptionField},
chgFn: combine(changeDescription("test-update-description-repo"), changeName("")),
want: &Account{
Account: &store.Account{
Name: "do-not-delete-name-test-name-repo",
Description: "test-update-description-repo",
},
},
wantCount: 1,
},
{
name: "do-not-delete-description",
repo: testRepo,
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "do-not-delete-description-test-name-repo",
Description: "test-description-repo",
},
},
masks: []string{NameField},
chgFn: combine(changeDescription(""), changeName("do-not-delete-description-test-update-name-repo")),
want: &Account{
Account: &store.Account{
Name: "do-not-delete-description-test-update-name-repo",
Description: "test-description-repo",
},
},
wantCount: 1,
},
{
name: "get-wrapper-err",
repo: func() *Repository {
kms := &mockGetWrapperer{
getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"),
}
r, err := NewRepository(testCtx, testRw, testRw, kms)
require.NoError(t, err)
return r
}(),
scopeId: org.GetPublicId(),
version: 1,
orig: &Account{
Account: &store.Account{
Name: "get-wrapper-err",
Description: "test-description-repo",
},
},
masks: []string{NameField},
chgFn: combine(changeDescription(""), changeName("get-wrapper-err-test-update-name-repo")),
wantIsErr: errors.Encrypt,
wantErrContains: "get-db-wrapper-err: encryption issue",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := TestAccount(t, testConn, am, tc.name, WithName(testCtx, tc.orig.GetName()), WithDescription(testCtx, tc.orig.GetDescription()))
tc.orig.AuthMethodId = am.PublicId
if tc.chgFn != nil {
orig = tc.chgFn(orig)
}
got, gotCount, err := tc.repo.UpdateAccount(context.Background(), tc.scopeId, orig, tc.version, tc.masks)
if tc.wantIsErr != 0 {
assert.Truef(errors.Match(errors.T(tc.wantIsErr), err), "want err: %q got: %q", tc.wantIsErr, err)
if tc.wantErrContains != "" {
assert.Contains(err.Error(), tc.wantErrContains)
}
assert.Equal(tc.wantCount, gotCount, "row count")
assert.Nil(got)
return
}
assert.NoError(err)
assert.Empty(tc.orig.PublicId)
if tc.wantCount == 0 {
assert.Equal(tc.wantCount, gotCount, "row count")
assert.Nil(got)
return
}
require.NotNil(got)
assertPublicId(t, globals.LdapAccountPrefix, got.PublicId)
assert.Equal(tc.wantCount, gotCount, "row count")
assert.NotSame(tc.orig, got)
assert.Equal(tc.orig.AuthMethodId, got.AuthMethodId)
underlyingDB, err := testConn.SqlDB(testCtx)
require.NoError(err)
dbassert := dbassert.New(t, underlyingDB)
if tc.want.Name == "" {
dbassert.IsNull(got, "name")
return
}
assert.Equal(tc.want.Name, got.Name)
if tc.want.Description == "" {
dbassert.IsNull(got, "description")
return
}
assert.Equal(tc.want.Description, got.Description)
if tc.wantCount > 0 {
assert.NoError(db.TestVerifyOplog(t, testRw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
}
})
}
}
func TestRepository_UpdateAccount_DupeNames(t *testing.T) {
testCtx := context.Background()
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testWrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, testConn, testWrapper)
iamRepo := iam.TestRepo(t, testConn, testWrapper)
t.Run("invalid-duplicate-names", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
name := "test-dup-name"
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
aa := TestAccount(t, testConn, am, "create-success1")
ab := TestAccount(t, testConn, am, "create-success2")
aa.Name = name
got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{NameField})
assert.NoError(err)
require.NotNil(got1)
assert.Equal(name, got1.Name)
assert.Equal(1, gotCount1, "row count")
assert.NoError(db.TestVerifyOplog(t, testRw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
ab.Name = name
got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{NameField})
assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err)
assert.Nil(got2)
assert.Equal(db.NoRowsAffected, gotCount2, "row count")
err = db.TestVerifyOplog(t, testRw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
assert.Error(err)
assert.True(errors.IsNotFoundError(err))
})
t.Run("valid-duplicate-names-diff-AuthMethods", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
ama := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
aa := TestAccount(t, testConn, ama, "create-success1", WithName(testCtx, "test-name-aa"))
amb := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"})
ab := TestAccount(t, testConn, amb, "create-success2", WithName(testCtx, "test-name-ab"))
ab.Name = aa.Name
got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{NameField})
assert.NoError(err)
require.NotNil(got3)
assert.NotSame(ab, got3)
assert.Equal(aa.Name, got3.Name)
assert.Equal(1, gotCount3, "row count")
assert.NoError(db.TestVerifyOplog(t, testRw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
})
t.Run("change-auth-method-id", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
ama := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
aa := TestAccount(t, testConn, ama, "create-success1")
amb := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"})
ab := TestAccount(t, testConn, amb, "create-success2")
assert.NotEqual(aa.AuthMethodId, ab.AuthMethodId)
orig := aa.clone()
aa.AuthMethodId = ab.AuthMethodId
assert.Equal(aa.AuthMethodId, ab.AuthMethodId)
got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{NameField})
assert.NoError(err)
require.NotNil(got1)
assert.Equal(orig.AuthMethodId, got1.AuthMethodId)
assert.Equal(1, gotCount1, "row count")
assert.NoError(db.TestVerifyOplog(t, testRw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
})
}
func assertPublicId(t *testing.T, prefix, actual string) {
t.Helper()
assert.NotEmpty(t, actual)
parts := strings.Split(actual, "_")
assert.Equalf(t, 2, len(parts), "want one '_' in PublicId, got multiple in %q", actual)
assert.Equalf(t, prefix, parts[0], "PublicId want prefix: %q, got: %q in %q", prefix, parts[0], actual)
}