Files
threshold/internal/clientcache/cmd/search/search_test.go
T
Johan Brandhorst-Satzkorn 1a6320794e cmd: propagate keyring reading errors (#5360)
Reading a token from the keyring can sometimes fail for reasons
other than the token not being present in the keyring. This PR
introduces a new error used to indicate that the token requested
could not be found in the keyring and propagates errors from the
keyring up to the caller, so that the caller can act on them.

Additionally, this fixes a bug in the client cache where it would
incorrectly assume that a token had been removed from the
keyring, when in fact there was an error reading from the keyring.
2024-12-23 09:21:18 -08:00

341 lines
9.3 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package search
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testCommander struct {
t *testing.T
at map[string]*authtokens.AuthToken
}
func (r *testCommander) Client(opt ...base.Option) (*api.Client, error) {
client, err := api.NewClient(nil)
require.NoError(r.t, err)
return client, nil
}
func (r *testCommander) ReadTokenFromKeyring(k, a string) (*authtokens.AuthToken, error) {
return r.at[a], nil
}
func TestSearch(t *testing.T) {
ctx := context.Background()
at := &authtokens.AuthToken{
Id: "at_1",
UserId: "user_1",
Token: "at_1_token",
ExpirationTime: time.Now().Add(time.Minute),
}
unsupportedAt := &authtokens.AuthToken{
Id: "at_2",
UserId: "user_2",
Token: "at_2_token",
ExpirationTime: time.Now().Add(time.Minute),
}
cmd := &testCommander{
t: t,
at: map[string]*authtokens.AuthToken{"tokenname": at, "unsupported": unsupportedAt},
}
boundaryTokenReaderFn := func(ctx context.Context, addr, authToken string) (*authtokens.AuthToken, error) {
switch authToken {
case at.Token:
return at, nil
case unsupportedAt.Token:
return unsupportedAt, nil
}
return nil, errors.New("test not found error")
}
readyNotificationCh := make(chan struct{})
srv := daemon.NewTestServer(t, cmd)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err := srv.Serve(
t,
daemon.WithBoundaryTokenReaderFunc(ctx, boundaryTokenReaderFn),
daemon.WithReadyToServeNotificationCh(context.Background(), readyNotificationCh),
)
if err != nil {
t.Error("Failed to serve daemon:", err)
}
}()
t.Cleanup(wg.Wait)
<-readyNotificationCh
srv.AddKeyringToken(t, "address", "keyringtype", "tokenname", at.Id, boundaryTokenReaderFn)
srv.AddKeyringToken(t, "address", "keyringtype", "unsupported", unsupportedAt.Id, boundaryTokenReaderFn)
errorCases := []struct {
name string
fb filterBy
apiErrContains string
}{
{
name: "no resource",
fb: filterBy{
flagQuery: "name=name",
authTokenId: at.Id,
},
apiErrContains: "resource is a required field but was empty",
},
{
name: "bad resource",
fb: filterBy{
authTokenId: at.Id,
flagQuery: "name=name",
resource: "hosts",
},
apiErrContains: "provided resource is not a valid searchable resource",
},
{
name: "unknown auth token id",
fb: filterBy{
authTokenId: "unknown",
flagQuery: "description % tar",
resource: "targets",
},
apiErrContains: "Forbidden",
},
{
name: "unsupported column",
fb: filterBy{
authTokenId: at.Id,
flagQuery: "item % 'tar'",
resource: "targets",
},
apiErrContains: "invalid column \"item\"",
},
}
for _, tc := range errorCases {
t.Run(tc.name, func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), tc.fb)
require.NoError(t, err)
assert.NotNil(t, apiErr)
assert.Contains(t, apiErr.Message, tc.apiErrContains)
assert.NotNil(t, resp)
assert.Nil(t, r)
})
}
t.Run("empty response from list", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.EqualValues(t, &daemon.SearchResult{
RefreshStatus: daemon.NotRefreshing,
}, r)
})
t.Run("empty response from query", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "name='name'",
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.EqualValues(t, r, &daemon.SearchResult{
RefreshStatus: daemon.NotRefreshing,
})
})
t.Run("unsupported boundary instance", func(t *testing.T) {
srv.AddUnsupportedCachingData(t, unsupportedAt, boundaryTokenReaderFn)
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: unsupportedAt.Id,
resource: "targets",
})
assert.NoError(t, err)
require.NotNil(t, apiErr)
assert.NotNil(t, resp)
assert.Nil(t, r)
assert.Contains(t, apiErr.Message, "doesn't support search")
})
srv.AddResources(t, at, []*aliases.Alias{
{Id: "alt_1234567890", Value: "value1", DestinationId: "ttcp_1234567890"},
{Id: "alt_0987654321", Name: "value2", DestinationId: "ttcp_0987654321"},
}, []*targets.Target{
{Id: "ttcp_1234567890", Name: "name1", Description: "description1"},
{Id: "ttcp_0987654321", Name: "name2", Description: "description2"},
}, []*sessions.Session{
{Id: "sess_1234567890", TargetId: "ttcp_1234567890", Status: "pending"},
{Id: "sess_0987654321", TargetId: "ttcp_0987654321", Status: "pending"},
}, boundaryTokenReaderFn)
t.Run("target response from list", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "targets",
})
require.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("search for unsupported controller", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("full target response from query", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % 'ttcp'",
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("partial target response from query", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % 'ttcp_1234567890'",
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 0)
assert.Len(t, r.Targets, 1)
})
t.Run("full target response from filter", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagFilter: `"/item/id" matches "ttcp"`,
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("partial target response from filter", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagFilter: `"/item/id" matches "ttcp_1234567890"`,
resource: "targets",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 0)
assert.Len(t, r.Targets, 1)
})
t.Run("session response from list", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "sessions",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 0)
assert.Len(t, r.Sessions, 2)
})
t.Run("full session response from query", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % 'sess'",
resource: "sessions",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 2)
})
t.Run("partial session response from query", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % 'sess_1234567890'",
resource: "sessions",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 1)
})
t.Run("full session response from filter", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagFilter: `"/item/id" matches "sess"`,
resource: "sessions",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 2)
})
t.Run("partial session response from filter", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagFilter: `"/item/id" matches "sess_1234567890"`,
resource: "sessions",
})
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 1)
})
}