diff --git a/testing/internal/e2e/boundary/role.go b/testing/internal/e2e/boundary/role.go index eb4bc42190..cb6f9193a8 100644 --- a/testing/internal/e2e/boundary/role.go +++ b/testing/internal/e2e/boundary/role.go @@ -8,11 +8,24 @@ import ( "encoding/json" "testing" + "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/roles" "github.com/hashicorp/boundary/testing/internal/e2e" "github.com/stretchr/testify/require" ) +// CreateNewRoleApi creates a new role using the Go api. +// Returns the id of the new role +func CreateNewRoleApi(t testing.TB, ctx context.Context, client *api.Client, scopeId string) string { + rClient := roles.NewClient(client) + newRoleResult, err := rClient.Create(ctx, scopeId) + require.NoError(t, err) + + newRoleId := newRoleResult.Item.Id + t.Logf("Created Role: %s", newRoleId) + return newRoleId +} + // CreateNewRoleCli creates a new role using the cli. // Returns the id of the new role. func CreateNewRoleCli(t testing.TB, ctx context.Context, scopeId string) string { diff --git a/testing/internal/e2e/tests/base/paginate_role_test.go b/testing/internal/e2e/tests/base/paginate_role_test.go new file mode 100644 index 0000000000..3a92c3d387 --- /dev/null +++ b/testing/internal/e2e/tests/base/paginate_role_test.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_test + +import ( + "context" + "encoding/json" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/api/roles" + "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" +) + +// TestCliPaginateRoles asserts that the CLI automatically paginates to retrieve +// all roles in a single invocation. +func TestCliPaginateRoles(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + 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)) + }) + + // Create enough roles to overflow a single page. + client, err := boundary.NewApiClient() + require.NoError(t, err) + // Creating an org comes with two automatically created roles, + // "Login and Default Grants" and "Administration", so we + // need to remove them from the total role count. + numPrecreatedRoles := 2 + var roleIds []string + for i := 0; i < c.MaxPageSize+1-numPrecreatedRoles; i++ { + roleIds = append(roleIds, boundary.CreateNewRoleApi(t, ctx, client, newOrgId)) + } + + // List roles + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "roles", "list", + "-scope-id", newOrgId, + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var initialRoles roles.RoleListResult + err = json.Unmarshal(output.Stdout, &initialRoles) + require.NoError(t, err) + + var returnedIds []string + // Ignore the two precreated roles, which will appear at the end + for _, role := range initialRoles.Items[:len(initialRoles.Items)-numPrecreatedRoles] { + returnedIds = append(returnedIds, role.Id) + } + + require.Len(t, initialRoles.Items, c.MaxPageSize+1) + assert.Empty(t, cmp.Diff(returnedIds, roleIds, cmpopts.SortSlices(func(i, j string) bool { return i < j }))) + assert.Empty(t, initialRoles.ResponseType) + assert.Empty(t, initialRoles.RemovedIds) + assert.Empty(t, initialRoles.ListToken) + + // Create a new role and destroy one of the other roles + newRoleId := boundary.CreateNewRoleApi(t, ctx, client, newOrgId) + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "roles", "delete", + "-id", initialRoles.Items[0].Id, + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // List again, should have the new role but not the deleted role + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "roles", "list", + "-scope-id", newOrgId, + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var newRoles roles.RoleListResult + err = json.Unmarshal(output.Stdout, &newRoles) + require.NoError(t, err) + + require.Len(t, newRoles.Items, c.MaxPageSize+1) + // The first item should be the most recently created, which + // should be our new role + firstItem := newRoles.Items[0] + assert.Equal(t, newRoleId, firstItem.Id) + assert.Empty(t, newRoles.ResponseType) + assert.Empty(t, newRoles.RemovedIds) + assert.Empty(t, newRoles.ListToken) + // Ensure the deleted role isn't returned + for _, role := range newRoles.Items { + assert.NotEqual(t, role.Id, initialRoles.Items[0].Id) + } +} + +// TestApiPaginateRoles asserts that the API automatically paginates to retrieve +// all roles in a single invocation. +func TestApiPaginateRoles(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + client, err := boundary.NewApiClient() + require.NoError(t, err) + ctx := context.Background() + sClient := scopes.NewClient(client) + uClient := roles.NewClient(client) + newOrgId := boundary.CreateNewOrgApi(t, ctx, client) + t.Cleanup(func() { + ctx := context.Background() + _, err := sClient.Delete(ctx, newOrgId) + require.NoError(t, err) + }) + + // Create enough roles to overflow a single page. + // Creating an org comes with two automatically created roles, + // "Login and Default Grants" and "Administration", so we + // need to remove them from the total role count. + numPrecreatedRoles := 2 + var roleIds []string + for i := 0; i < c.MaxPageSize+1-numPrecreatedRoles; i++ { + roleIds = append(roleIds, boundary.CreateNewRoleApi(t, ctx, client, newOrgId)) + } + + // List roles + initialRoles, err := uClient.List(ctx, newOrgId) + require.NoError(t, err) + + var returnedIds []string + // Ignore the two precreated roles, which will appear at the end + for _, role := range initialRoles.Items[:len(initialRoles.Items)-numPrecreatedRoles] { + returnedIds = append(returnedIds, role.Id) + } + + require.Len(t, initialRoles.Items, c.MaxPageSize+1) + assert.Empty(t, cmp.Diff(returnedIds, roleIds, cmpopts.SortSlices(func(i, j string) bool { return i < j }))) + assert.Equal(t, "complete", initialRoles.ResponseType) + assert.Empty(t, initialRoles.RemovedIds) + assert.NotEmpty(t, initialRoles.ListToken) + mapItems, ok := initialRoles.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 role and destroy one of the other roles + newRoleId := boundary.CreateNewRoleApi(t, ctx, client, newOrgId) + _, err = uClient.Delete(ctx, initialRoles.Items[0].Id) + require.NoError(t, err) + + // List again, should have the new and deleted role + newRoles, err := uClient.List(ctx, newOrgId, roles.WithListToken(initialRoles.ListToken)) + require.NoError(t, err) + + // Note that this will likely contain all the roles, + // 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(newRoles.Items), 1) + // The first item should be the most recently created, which + // should be our new role + firstItem := newRoles.Items[0] + assert.Equal(t, newRoleId, firstItem.Id) + assert.Equal(t, "complete", newRoles.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(newRoles.RemovedIds), 1) + assert.True(t, slices.ContainsFunc(newRoles.RemovedIds, func(roleId string) bool { + return roleId == initialRoles.Items[0].Id + })) + assert.NotEmpty(t, newRoles.ListToken) + // Check that the response map contains all entries + mapItems, ok = newRoles.GetResponse().Map["items"] + require.True(t, ok) + mapSliceItems, ok = mapItems.([]any) + require.True(t, ok) + assert.GreaterOrEqual(t, len(mapSliceItems), 1) +}