// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package handlers_test import ( "context" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/listtoken" "github.com/hashicorp/boundary/internal/types/resource" "github.com/mr-tron/base58" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" ) func Test_ParseListToken(t *testing.T) { t.Parallel() fiveDaysAgo := time.Now().AddDate(0, 0, -5) tokenCreateTime := fiveDaysAgo lastItemCreateTime := fiveDaysAgo.Add(-2 * time.Hour) lastItemUpdateTime := fiveDaysAgo.Add(-time.Hour) previousPhaseUpperBoundTime := fiveDaysAgo.Add(4 * time.Hour) previousDeletedIdsTime := fiveDaysAgo.Add(3 * time.Hour) phaseLowerBound := fiveDaysAgo.Add(time.Hour) phaseUpperBound := fiveDaysAgo.Add(2 * time.Hour) t.Run("valid pagination", func(t *testing.T) { t.Parallel() pagToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_PaginationToken{ PaginationToken: &pbs.PaginationToken{ LastItemId: "ttcp_1234567890", LastItemCreateTime: timestamppb.New(lastItemCreateTime), }, }, } pagBytes, err := proto.Marshal(pagToken) require.NoError(t, err) pagString := base58.Encode(pagBytes) domainPagToken, err := listtoken.NewPagination( context.Background(), pagToken.CreateTime.AsTime(), handlers.ListTokenResourceToResource(pagToken.ResourceType), pagToken.GrantsHash, pagToken.GetPaginationToken().LastItemId, pagToken.GetPaginationToken().LastItemCreateTime.AsTime(), ) require.NoError(t, err) got, err := handlers.ParseListToken(context.Background(), pagString, resource.Target, []byte("some hash")) require.NoError(t, err) require.Empty(t, cmp.Diff(got, domainPagToken, protocmp.Transform())) }) t.Run("valid start-refresh", func(t *testing.T) { t.Parallel() srToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_StartRefreshToken{ StartRefreshToken: &pbs.StartRefreshToken{ PreviousPhaseUpperBound: timestamppb.New(previousPhaseUpperBoundTime), PreviousDeletedIdsTime: timestamppb.New(previousDeletedIdsTime), }, }, } srBytes, err := proto.Marshal(srToken) require.NoError(t, err) srString := base58.Encode(srBytes) domainSrToken, err := listtoken.NewStartRefresh( context.Background(), srToken.CreateTime.AsTime(), handlers.ListTokenResourceToResource(srToken.ResourceType), srToken.GrantsHash, srToken.GetStartRefreshToken().PreviousDeletedIdsTime.AsTime(), srToken.GetStartRefreshToken().PreviousPhaseUpperBound.AsTime(), ) require.NoError(t, err) got, err := handlers.ParseListToken(context.Background(), srString, resource.Target, []byte("some hash")) require.NoError(t, err) require.Empty(t, cmp.Diff(got, domainSrToken, protocmp.Transform())) }) t.Run("valid refresh", func(t *testing.T) { t.Parallel() rToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_RefreshToken{ RefreshToken: &pbs.RefreshToken{ PhaseUpperBound: timestamppb.New(phaseUpperBound), PhaseLowerBound: timestamppb.New(phaseLowerBound), PreviousDeletedIdsTime: timestamppb.New(previousDeletedIdsTime), LastItemId: "ttcp_1234567890", LastItemUpdateTime: timestamppb.New(lastItemUpdateTime), }, }, } rBytes, err := proto.Marshal(rToken) require.NoError(t, err) rString := base58.Encode(rBytes) domainRToken, err := listtoken.NewRefresh( context.Background(), tokenCreateTime, handlers.ListTokenResourceToResource(rToken.ResourceType), rToken.GrantsHash, rToken.GetRefreshToken().PreviousDeletedIdsTime.AsTime(), rToken.GetRefreshToken().PhaseUpperBound.AsTime(), rToken.GetRefreshToken().PhaseLowerBound.AsTime(), rToken.GetRefreshToken().LastItemId, rToken.GetRefreshToken().LastItemUpdateTime.AsTime(), ) require.NoError(t, err) got, err := handlers.ParseListToken(context.Background(), rString, resource.Target, []byte("some hash")) require.NoError(t, err) require.Empty(t, cmp.Diff(got, domainRToken, protocmp.Transform())) }) t.Run("empty token", func(t *testing.T) { t.Parallel() _, err := handlers.ParseListToken(context.Background(), "", resource.Target, []byte("some hash")) require.Error(t, err) }) t.Run("invalid base58", func(t *testing.T) { t.Parallel() _, err := handlers.ParseListToken(context.Background(), "not a token", resource.Target, []byte("some hash")) require.Error(t, err) }) t.Run("invalid proto", func(t *testing.T) { t.Parallel() _, err := handlers.ParseListToken(context.Background(), base58.Encode([]byte("not a token")), resource.Target, []byte("some hash")) require.Error(t, err) }) t.Run("resource type mismatch", func(t *testing.T) { t.Parallel() pagToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_PaginationToken{ PaginationToken: &pbs.PaginationToken{ LastItemId: "ttcp_1234567890", LastItemCreateTime: timestamppb.New(lastItemCreateTime), }, }, } pagBytes, err := proto.Marshal(pagToken) require.NoError(t, err) pagString := base58.Encode(pagBytes) _, err = handlers.ParseListToken(context.Background(), pagString, resource.Session, []byte("some hash")) require.Error(t, err) }) t.Run("grants hash mismatch", func(t *testing.T) { t.Parallel() pagToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_PaginationToken{ PaginationToken: &pbs.PaginationToken{ LastItemId: "ttcp_1234567890", LastItemCreateTime: timestamppb.New(lastItemCreateTime), }, }, } pagBytes, err := proto.Marshal(pagToken) require.NoError(t, err) pagString := base58.Encode(pagBytes) _, err = handlers.ParseListToken(context.Background(), pagString, resource.Target, []byte("some other hash")) require.Error(t, err) }) } func Test_MarshalListToken(t *testing.T) { t.Parallel() fiveDaysAgo := time.Now().AddDate(0, 0, -5) tokenCreateTime := fiveDaysAgo lastItemCreateTime := fiveDaysAgo.Add(time.Hour) lastItemUpdateTime := fiveDaysAgo.Add(2 * time.Hour) previousPhaseUpperBoundTime := fiveDaysAgo.Add(3 * time.Hour) previousDeletedIdsTime := fiveDaysAgo.Add(4 * time.Hour) phaseLowerBound := fiveDaysAgo.Add(5 * time.Hour) phaseUpperBound := fiveDaysAgo.Add(6 * time.Hour) t.Run("valid pagination", func(t *testing.T) { t.Parallel() pagToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_PaginationToken{ PaginationToken: &pbs.PaginationToken{ LastItemId: "ttcp_1234567890", LastItemCreateTime: timestamppb.New(lastItemCreateTime), }, }, } pagBytes, err := proto.Marshal(pagToken) require.NoError(t, err) pagString := base58.Encode(pagBytes) domainPagToken, err := listtoken.NewPagination( context.Background(), pagToken.CreateTime.AsTime(), handlers.ListTokenResourceToResource(pagToken.ResourceType), pagToken.GrantsHash, pagToken.GetPaginationToken().LastItemId, pagToken.GetPaginationToken().LastItemCreateTime.AsTime(), ) require.NoError(t, err) got, err := handlers.MarshalListToken(context.Background(), domainPagToken, pbs.ResourceType_RESOURCE_TYPE_TARGET) require.NoError(t, err) require.Empty(t, cmp.Diff(got, pagString, protocmp.Transform())) }) t.Run("valid start-refresh", func(t *testing.T) { t.Parallel() srToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_StartRefreshToken{ StartRefreshToken: &pbs.StartRefreshToken{ PreviousPhaseUpperBound: timestamppb.New(previousPhaseUpperBoundTime), PreviousDeletedIdsTime: timestamppb.New(previousDeletedIdsTime), }, }, } srBytes, err := proto.Marshal(srToken) require.NoError(t, err) srString := base58.Encode(srBytes) domainSrToken, err := listtoken.NewStartRefresh( context.Background(), srToken.CreateTime.AsTime(), handlers.ListTokenResourceToResource(srToken.ResourceType), srToken.GrantsHash, srToken.GetStartRefreshToken().PreviousDeletedIdsTime.AsTime(), srToken.GetStartRefreshToken().PreviousPhaseUpperBound.AsTime(), ) require.NoError(t, err) got, err := handlers.MarshalListToken(context.Background(), domainSrToken, pbs.ResourceType_RESOURCE_TYPE_TARGET) require.NoError(t, err) require.Empty(t, cmp.Diff(got, srString, protocmp.Transform())) }) t.Run("valid refresh", func(t *testing.T) { t.Parallel() rToken := &pbs.ListToken{ CreateTime: timestamppb.New(tokenCreateTime), ResourceType: pbs.ResourceType_RESOURCE_TYPE_TARGET, GrantsHash: []byte("some hash"), Token: &pbs.ListToken_RefreshToken{ RefreshToken: &pbs.RefreshToken{ PhaseUpperBound: timestamppb.New(phaseUpperBound), PhaseLowerBound: timestamppb.New(phaseLowerBound), PreviousDeletedIdsTime: timestamppb.New(previousDeletedIdsTime), LastItemId: "ttcp_1234567890", LastItemUpdateTime: timestamppb.New(lastItemUpdateTime), }, }, } rBytes, err := proto.Marshal(rToken) require.NoError(t, err) rString := base58.Encode(rBytes) domainRToken, err := listtoken.NewRefresh( context.Background(), tokenCreateTime, handlers.ListTokenResourceToResource(rToken.ResourceType), rToken.GrantsHash, rToken.GetRefreshToken().PreviousDeletedIdsTime.AsTime(), rToken.GetRefreshToken().PhaseUpperBound.AsTime(), rToken.GetRefreshToken().PhaseLowerBound.AsTime(), rToken.GetRefreshToken().LastItemId, rToken.GetRefreshToken().LastItemUpdateTime.AsTime(), ) require.NoError(t, err) got, err := handlers.MarshalListToken(context.Background(), domainRToken, pbs.ResourceType_RESOURCE_TYPE_TARGET) require.NoError(t, err) require.Empty(t, cmp.Diff(got, rString, protocmp.Transform())) }) t.Run("nil token", func(t *testing.T) { t.Parallel() _, err := handlers.MarshalListToken(context.Background(), nil, pbs.ResourceType_RESOURCE_TYPE_TARGET) require.Error(t, err) }) t.Run("invalid token resource type", func(t *testing.T) { t.Parallel() domainPagToken, err := listtoken.NewPagination( context.Background(), tokenCreateTime, resource.Target, []byte("some hash"), "ttcp_1234567890", lastItemCreateTime, ) require.NoError(t, err) _, err = handlers.MarshalListToken(context.Background(), domainPagToken, pbs.ResourceType_RESOURCE_TYPE_UNSPECIFIED) require.Error(t, err) }) t.Run("token resource type mismatch", func(t *testing.T) { t.Parallel() domainPagToken, err := listtoken.NewPagination( context.Background(), tokenCreateTime, resource.Target, []byte("some hash"), "ttcp_1234567890", lastItemCreateTime, ) require.NoError(t, err) _, err = handlers.MarshalListToken(context.Background(), domainPagToken, pbs.ResourceType_RESOURCE_TYPE_SESSION) require.Error(t, err) }) t.Run("invalid token subtype", func(t *testing.T) { t.Parallel() invalidToken := &listtoken.Token{ CreateTime: tokenCreateTime, ResourceType: resource.Target, GrantsHash: []byte("some hash"), } _, err := handlers.MarshalListToken(context.Background(), invalidToken, pbs.ResourceType_RESOURCE_TYPE_TARGET) require.Error(t, err) }) } func TestListTokenResourceToResource(t *testing.T) { t.Parallel() tests := []struct { name string rt pbs.ResourceType want resource.Type }{ { name: "default unknown", rt: 0, want: resource.Unknown, }, { name: "account", rt: pbs.ResourceType_RESOURCE_TYPE_ACCOUNT, want: resource.Account, }, { name: "alias", rt: pbs.ResourceType_RESOURCE_TYPE_ALIAS, want: resource.Alias, }, { name: "auth_method", rt: pbs.ResourceType_RESOURCE_TYPE_AUTH_METHOD, want: resource.AuthMethod, }, { name: "auth_token", rt: pbs.ResourceType_RESOURCE_TYPE_AUTH_TOKEN, want: resource.AuthToken, }, { name: "credential_library", rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_LIBRARY, want: resource.CredentialLibrary, }, { name: "credential_store", rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL_STORE, want: resource.CredentialStore, }, { name: "credential", rt: pbs.ResourceType_RESOURCE_TYPE_CREDENTIAL, want: resource.Credential, }, { name: "group", rt: pbs.ResourceType_RESOURCE_TYPE_GROUP, want: resource.Group, }, { name: "host_catalog", rt: pbs.ResourceType_RESOURCE_TYPE_HOST_CATALOG, want: resource.HostCatalog, }, { name: "host_set", rt: pbs.ResourceType_RESOURCE_TYPE_HOST_SET, want: resource.HostSet, }, { name: "host", rt: pbs.ResourceType_RESOURCE_TYPE_HOST, want: resource.Host, }, { name: "managed_group", rt: pbs.ResourceType_RESOURCE_TYPE_MANAGED_GROUP, want: resource.ManagedGroup, }, { name: "role", rt: pbs.ResourceType_RESOURCE_TYPE_ROLE, want: resource.Role, }, { name: "scope", rt: pbs.ResourceType_RESOURCE_TYPE_SCOPE, want: resource.Scope, }, { name: "session_recording", rt: pbs.ResourceType_RESOURCE_TYPE_SESSION_RECORDING, want: resource.SessionRecording, }, { name: "session", rt: pbs.ResourceType_RESOURCE_TYPE_SESSION, want: resource.Session, }, { name: "storage_bucket", rt: pbs.ResourceType_RESOURCE_TYPE_STORAGE_BUCKET, want: resource.StorageBucket, }, { name: "target", rt: pbs.ResourceType_RESOURCE_TYPE_TARGET, want: resource.Target, }, { name: "user", rt: pbs.ResourceType_RESOURCE_TYPE_USER, want: resource.User, }, { name: "worker", rt: pbs.ResourceType_RESOURCE_TYPE_WORKER, want: resource.Worker, }, { name: "policy", rt: pbs.ResourceType_RESOURCE_TYPE_POLICY, want: resource.Policy, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := handlers.ListTokenResourceToResource(tt.rt) require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform())) }) } }