From 0bddb346e5ba2e2cdb6a6f302296738e78ea4f73 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Mon, 29 Jan 2024 13:22:21 -0800 Subject: [PATCH] testing/e2e: add account pagination test (#4314) --- testing/internal/e2e/boundary/account.go | 9 +- .../e2e/tests/base/paginate_account_test.go | 209 ++++++++++++++++++ .../tests/base/session_cancel_group_test.go | 4 +- .../tests/base/session_cancel_user_test.go | 4 +- 4 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 testing/internal/e2e/tests/base/paginate_account_test.go diff --git a/testing/internal/e2e/boundary/account.go b/testing/internal/e2e/boundary/account.go index 097da8d7a8..7ce7fd681e 100644 --- a/testing/internal/e2e/boundary/account.go +++ b/testing/internal/e2e/boundary/account.go @@ -17,14 +17,11 @@ import ( // CreateNewAccountApi creates a new account using the Go api. // Returns the id of the new account as well as the password that was generated -func CreateNewAccountApi(t testing.TB, ctx context.Context, client *api.Client, loginName string) (accountId string, password string) { - c, err := LoadConfig() - require.NoError(t, err) - +func CreateNewAccountApi(t testing.TB, ctx context.Context, client *api.Client, authMethodId string, loginName string) (accountId string, password string) { aClient := accounts.NewClient(client) - password, err = base62.Random(16) + password, err := base62.Random(16) require.NoError(t, err) - newAccountResult, err := aClient.Create(ctx, c.AuthMethodId, + newAccountResult, err := aClient.Create(ctx, authMethodId, accounts.WithPasswordAccountLoginName(loginName), accounts.WithPasswordAccountPassword(password), ) diff --git a/testing/internal/e2e/tests/base/paginate_account_test.go b/testing/internal/e2e/tests/base/paginate_account_test.go new file mode 100644 index 0000000000..be697cc26c --- /dev/null +++ b/testing/internal/e2e/tests/base/paginate_account_test.go @@ -0,0 +1,209 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_test + +import ( + "context" + "encoding/json" + "slices" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/api/accounts" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCliPaginateAccounts asserts that the CLI automatically paginates to retrieve +// all accounts in a single invocation. +func TestCliPaginateAccounts(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + client, err := boundary.NewApiClient() + require.NoError(t, err) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + amId := boundary.CreateNewAuthMethodApi(t, ctx, client, newOrgId) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("auth-methods", "delete", "-id", amId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create enough accounts to overflow a single page. + var accountIds []string + for i := 0; i < c.MaxPageSize+1; i++ { + accId, _ := boundary.CreateNewAccountApi(t, ctx, client, amId, "testuser-"+strconv.Itoa(i)) + accountIds = append(accountIds, accId) + } + + // List accounts + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "accounts", "list", + "-auth-method-id", amId, + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var initialAccounts accounts.AccountListResult + err = json.Unmarshal(output.Stdout, &initialAccounts) + require.NoError(t, err) + + var returnedIds []string + for _, account := range initialAccounts.Items { + returnedIds = append(returnedIds, account.Id) + } + + require.Len(t, initialAccounts.Items, c.MaxPageSize+1) + assert.Empty(t, cmp.Diff(returnedIds, accountIds, cmpopts.SortSlices(func(i, j string) bool { return i < j }))) + assert.Empty(t, initialAccounts.ResponseType) + assert.Empty(t, initialAccounts.RemovedIds) + assert.Empty(t, initialAccounts.ListToken) + + // Create a new account and destroy one of the other accounts + newAccountId, _ := boundary.CreateNewAccountApi(t, ctx, client, amId, "newuser") + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "accounts", "delete", + "-id", initialAccounts.Items[0].Id, + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // List again, should have the new account but not the deleted account + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "accounts", "list", + "-auth-method-id", amId, + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var newAccounts accounts.AccountListResult + err = json.Unmarshal(output.Stdout, &newAccounts) + require.NoError(t, err) + + require.Len(t, newAccounts.Items, c.MaxPageSize+1) + // The first item should be the most recently created, which + // should be our new account + firstItem := newAccounts.Items[0] + assert.Equal(t, newAccountId, firstItem.Id) + assert.Empty(t, newAccounts.ResponseType) + assert.Empty(t, newAccounts.RemovedIds) + assert.Empty(t, newAccounts.ListToken) + // Ensure the deleted account isn't returned + for _, account := range newAccounts.Items { + assert.NotEqual(t, account.Id, initialAccounts.Items[0].Id) + } +} + +// TestApiPaginateAccounts asserts that the API automatically paginates to retrieve +// all accounts in a single invocation. +func TestApiPaginateAccounts(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + client, err := boundary.NewApiClient() + require.NoError(t, err) + adminToken := client.Token() + ctx := context.Background() + sClient := scopes.NewClient(client) + amClient := authmethods.NewClient(client) + acClient := accounts.NewClient(client) + newOrgId := boundary.CreateNewOrgApi(t, ctx, client) + t.Cleanup(func() { + ctx := context.Background() + client.SetToken(adminToken) + _, err = sClient.Delete(ctx, newOrgId) + require.NoError(t, err) + }) + amId := boundary.CreateNewAuthMethodApi(t, ctx, client, newOrgId) + t.Cleanup(func() { + ctx := context.Background() + client.SetToken(adminToken) + _, err := amClient.Delete(ctx, amId) + require.NoError(t, err) + }) + + // Create enough accounts to overflow a single page. + var accountIds []string + for i := 0; i < c.MaxPageSize+1; i++ { + accId, _ := boundary.CreateNewAccountApi(t, ctx, client, amId, "testuser-"+strconv.Itoa(i)) + accountIds = append(accountIds, accId) + } + + // List accounts + initialAccounts, err := acClient.List(ctx, amId) + require.NoError(t, err) + + var returnedIds []string + for _, account := range initialAccounts.Items { + returnedIds = append(returnedIds, account.Id) + } + + require.Len(t, initialAccounts.Items, c.MaxPageSize+1) + assert.Empty(t, cmp.Diff(returnedIds, accountIds, cmpopts.SortSlices(func(i, j string) bool { return i < j }))) + assert.Equal(t, "complete", initialAccounts.ResponseType) + assert.Empty(t, initialAccounts.RemovedIds) + assert.NotEmpty(t, initialAccounts.ListToken) + mapItems, ok := initialAccounts.GetResponse().Map["items"] + require.True(t, ok) + mapSliceItems, ok := mapItems.([]any) + require.True(t, ok) + assert.Len(t, mapSliceItems, c.MaxPageSize+1) + + // Create a new account and destroy one of the other accounts + newAccountId, _ := boundary.CreateNewAccountApi(t, ctx, client, amId, "newuser") + _, err = acClient.Delete(ctx, initialAccounts.Items[0].Id) + require.NoError(t, err) + + // List again, should have the new and deleted account + newAccounts, err := acClient.List(ctx, amId, accounts.WithListToken(initialAccounts.ListToken)) + require.NoError(t, err) + + // Note that this will likely contain all the accounts, + // since they were created very shortly before the listing, + // and we add a 30 second buffer to the lower bound of update + // times when listing. + require.GreaterOrEqual(t, len(newAccounts.Items), 1) + // The first item should be the most recently created, which + // should be our new account + firstItem := newAccounts.Items[0] + assert.Equal(t, newAccountId, firstItem.Id) + assert.Equal(t, "complete", newAccounts.ResponseType) + // Note that the removed IDs may contain entries from other tests, + // so just check that there is at least 1 entry and that our entry + // is somewhere in the list. + require.GreaterOrEqual(t, len(newAccounts.RemovedIds), 1) + assert.True(t, slices.ContainsFunc(newAccounts.RemovedIds, func(accountId string) bool { + return accountId == initialAccounts.Items[0].Id + })) + assert.NotEmpty(t, newAccounts.ListToken) + // Check that the response map contains all entries + mapItems, ok = newAccounts.GetResponse().Map["items"] + require.True(t, ok) + mapSliceItems, ok = mapItems.([]any) + require.True(t, ok) + assert.GreaterOrEqual(t, len(mapSliceItems), 1) +} diff --git a/testing/internal/e2e/tests/base/session_cancel_group_test.go b/testing/internal/e2e/tests/base/session_cancel_group_test.go index feeb588cf4..506c0f690f 100644 --- a/testing/internal/e2e/tests/base/session_cancel_group_test.go +++ b/testing/internal/e2e/tests/base/session_cancel_group_test.go @@ -157,6 +157,8 @@ func TestApiCreateGroup(t *testing.T) { e2e.MaybeSkipTest(t) c, err := loadTestConfig() require.NoError(t, err) + bc, err := boundary.LoadConfig() + require.NoError(t, err) client, err := boundary.NewApiClient() require.NoError(t, err) @@ -177,7 +179,7 @@ func TestApiCreateGroup(t *testing.T) { boundary.AddHostSourceToTargetApi(t, ctx, client, newTargetId, newHostSetId) acctName := "e2e-account" - newAcctId, _ := boundary.CreateNewAccountApi(t, ctx, client, acctName) + newAcctId, _ := boundary.CreateNewAccountApi(t, ctx, client, bc.AuthMethodId, acctName) t.Cleanup(func() { aClient := accounts.NewClient(client) _, err := aClient.Delete(ctx, newAcctId) diff --git a/testing/internal/e2e/tests/base/session_cancel_user_test.go b/testing/internal/e2e/tests/base/session_cancel_user_test.go index dd71977e7d..283eda0952 100644 --- a/testing/internal/e2e/tests/base/session_cancel_user_test.go +++ b/testing/internal/e2e/tests/base/session_cancel_user_test.go @@ -152,6 +152,8 @@ func TestApiCreateUser(t *testing.T) { e2e.MaybeSkipTest(t) c, err := loadTestConfig() require.NoError(t, err) + bc, err := boundary.LoadConfig() + require.NoError(t, err) client, err := boundary.NewApiClient() require.NoError(t, err) @@ -172,7 +174,7 @@ func TestApiCreateUser(t *testing.T) { boundary.AddHostSourceToTargetApi(t, ctx, client, newTargetId, newHostSetId) acctName := "e2e-account" - newAcctId, _ := boundary.CreateNewAccountApi(t, ctx, client, acctName) + newAcctId, _ := boundary.CreateNewAccountApi(t, ctx, client, bc.AuthMethodId, acctName) t.Cleanup(func() { aClient := accounts.NewClient(client) _, err := aClient.Delete(ctx, newAcctId)