feat(clientcache): Implement session sorting in repository (#6348, #6378)

This commit is contained in:
Sepehr committed 2026-02-03 09:18:16 -08:00
1 parent 7f1a993122
commit 8201630b12
6 files changed
+1034 -14

No files matched your search

@@ -384,7 +384,7 @@ func TestSearch(t *testing.T) {
assert.Less(t, r.Sessions[1].CreatedTime, r.Sessions[0].CreatedTime)
})
t.Run("sorted sessions ascending by default", func(t *testing.T) {
t.Run("sorted sessions descending by default", func(t *testing.T) {
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "sessions",
@@ -394,7 +394,7 @@ func TestSearch(t *testing.T) {
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Less(t, r.Sessions[0].CreatedTime, r.Sessions[1].CreatedTime)
assert.Less(t, r.Sessions[1].CreatedTime, r.Sessions[0].CreatedTime)
})
t.Run("sorted and filtered", func(t *testing.T) {
+5 -5
View File
@@ -139,23 +139,23 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithSort-valid-name-ascending", func(t *testing.T) {
opts, err := getOpts(WithSort(SortByName, Ascending, []SortBy{SortByName, SortByCreatedAt}))
opts, err := getOpts(WithSort(SortByName, Ascending, []SortBy{SortByName, SortByCreatedTime}))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withSortBy = SortByName
testOpts.withSortDirection = Ascending
assert.Equal(t, opts, testOpts)
})
t.Run("WithSort-valid-created_at-descending", func(t *testing.T) {
opts, err := getOpts(WithSort(SortByCreatedAt, Descending, []SortBy{SortByCreatedAt}))
t.Run("WithSort-valid-created_time-descending", func(t *testing.T) {
opts, err := getOpts(WithSort(SortByCreatedTime, Descending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withSortBy = SortByCreatedAt
testOpts.withSortBy = SortByCreatedTime
testOpts.withSortDirection = Descending
assert.Equal(t, opts, testOpts)
})
t.Run("WithSort-column-not-in-sortable-list", func(t *testing.T) {
_, err := getOpts(WithSort(SortByName, Ascending, []SortBy{SortByCreatedAt}))
_, err := getOpts(WithSort(SortByName, Ascending, []SortBy{SortByCreatedTime}))
require.Error(t, err)
assert.ErrorContains(t, err, errInvalidSortColumn.Error())
})
+32 -1
View File
@@ -19,6 +19,13 @@ import (
"github.com/hashicorp/mql"
)
// sessionSortFieldMap maps SortBy values to their corresponding JSON field paths.
// Sessions store data in a JSON blob, requiring json_extract for sorting.
var sessionSortFieldMap = map[SortBy]string{
SortByCreatedTime: "created_time",
// Add future sortable fields here
}
// SessionRetrievalFunc is a function that retrieves sessions
// from the provided boundary addr using the provided token.
type SessionRetrievalFunc func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue, inPage *sessions.SessionListResult, opt ...Option) (ret *sessions.SessionListResult, refreshToken RefreshTokenValue, err error)
@@ -373,8 +380,32 @@ func (r *Repository) searchSessions(ctx context.Context, condition string, searc
searchArgs = append(searchArgs, opts.withUserId)
}
dbOpts := []db.Option{db.WithLimit(opts.withMaxResultSetSize + 1)}
if opts.withSortBy != SortByDefault {
var direction string
switch opts.withSortDirection {
// default to descending
case Descending, SortDirectionDefault:
direction = "desc"
case Ascending:
direction = "asc"
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported sort direction: %v", opts.withSortDirection))
}
jsonField, ok := sessionSortFieldMap[opts.withSortBy]
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported sort field for sessions: %v", opts.withSortBy))
}
// Sessions store data in a JSON blob (i.e. created_time), so we need to use json_extract for sorting
orderClause := fmt.Sprintf("json_extract(item, '$.%s') %s", jsonField, direction)
dbOpts = append(dbOpts, db.WithOrder(orderClause))
}
var cachedSessions []*Session
if err := r.rw.SearchWhere(ctx, &cachedSessions, condition, searchArgs, db.WithLimit(opts.withMaxResultSetSize+1)); err != nil {
if err := r.rw.SearchWhere(ctx, &cachedSessions, condition, searchArgs, dbOpts...); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
@@ -6,6 +6,7 @@ package cache
import (
"context"
"encoding/json"
"fmt"
"strconv"
"sync"
"testing"
@@ -643,3 +644,991 @@ func TestDefaultSessionRetrievalFunc(t *testing.T) {
assert.Empty(t, got2.RemovedIds)
assert.Empty(t, got2.Items)
}
func TestRepository_SearchSessionsSorting(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create sessions with different created_time times
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "status1",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: time.Now().Add(-2 * time.Hour),
},
{
Id: "ttcp_2",
Status: "status2",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: time.Now().Add(-1 * time.Hour),
},
{
Id: "ttcp_3",
Status: "status3",
Endpoint: "address3",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: time.Now(),
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
searchService, err := NewSearchService(ctx, r)
require.NoError(t, err)
t.Run("sort by created_time ascending", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
SortBy: SortByCreatedTime,
SortDirection: Ascending,
}
result, err := searchService.Search(ctx, params)
require.NoError(t, err)
require.Len(t, result.Sessions, 3)
assert.Equal(t, "ttcp_1", result.Sessions[0].Id)
assert.Equal(t, "ttcp_2", result.Sessions[1].Id)
assert.Equal(t, "ttcp_3", result.Sessions[2].Id)
})
t.Run("sort by created_time descending", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
SortBy: SortByCreatedTime,
SortDirection: Descending,
}
result, err := searchService.Search(ctx, params)
require.NoError(t, err)
require.Len(t, result.Sessions, 3)
assert.Equal(t, "ttcp_3", result.Sessions[0].Id)
assert.Equal(t, "ttcp_2", result.Sessions[1].Id)
assert.Equal(t, "ttcp_1", result.Sessions[2].Id)
})
t.Run("sort by created_time with default direction defaults to desc", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
SortBy: SortByCreatedTime,
SortDirection: SortDirectionDefault,
}
result, err := searchService.Search(ctx, params)
require.NoError(t, err)
require.Len(t, result.Sessions, 3)
assert.Equal(t, "ttcp_3", result.Sessions[0].Id)
assert.Equal(t, "ttcp_2", result.Sessions[1].Id)
assert.Equal(t, "ttcp_1", result.Sessions[2].Id)
})
t.Run("no sort specified returns results", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
}
result, err := searchService.Search(ctx, params)
require.NoError(t, err)
assert.Len(t, result.Sessions, 3)
})
t.Run("invalid sort column for sessions returns error", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
SortBy: SortByName, // name is not valid for sessions
SortDirection: Ascending,
}
_, err := searchService.Search(ctx, params)
require.Error(t, err)
assert.ErrorContains(t, err, errInvalidSortColumn.Error())
})
t.Run("sort with query", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
Query: `(status % "status")`,
SortBy: SortByCreatedTime,
SortDirection: Descending,
}
result, err := searchService.Search(ctx, params)
require.NoError(t, err)
require.Len(t, result.Sessions, 3)
assert.Equal(t, "ttcp_3", result.Sessions[0].Id)
assert.Equal(t, "ttcp_2", result.Sessions[1].Id)
assert.Equal(t, "ttcp_1", result.Sessions[2].Id)
})
t.Run("sort with limit", func(t *testing.T) {
params := SearchParams{
Resource: Sessions,
AuthTokenId: kt.AuthTokenId,
SortBy: SortByCreatedTime,
SortDirection: Descending,
MaxResultSetSize: 2,
}
result, err := searchService.Search(ctx, params)
require.NoError(t, err)
assert.Len(t, result.Sessions, 2)
assert.True(t, result.Incomplete)
assert.Equal(t, "ttcp_3", result.Sessions[0].Id)
assert.Equal(t, "ttcp_2", result.Sessions[1].Id)
})
}
func TestRepository_searchSessions_dbOptions(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create sessions with different created_time values for sorting tests
// These timestamps are intentionally different to verify sorting works correctly
now := time.Now()
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "status1",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-2 * time.Hour), // Oldest
},
{
Id: "ttcp_2",
Status: "status2",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-1 * time.Hour), // Middle
},
{
Id: "ttcp_3",
Status: "status3",
Endpoint: "address3",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now, // Newest
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
tests := []struct {
name string
sortBy SortBy
sortDirection SortDirection
maxResultSetSize int
expectedOrder []string // expected session IDs in order - verifies json_extract sorting works
expectIncomplete bool
}{
{
name: "default - no sorting",
sortBy: SortByDefault,
sortDirection: SortDirectionDefault,
maxResultSetSize: 10,
expectIncomplete: false,
},
{
name: "sort by created_time ascending - verifies json_extract asc works",
sortBy: SortByCreatedTime,
sortDirection: Ascending,
maxResultSetSize: 10,
expectedOrder: []string{"ttcp_1", "ttcp_2", "ttcp_3"}, // oldest to newest
expectIncomplete: false,
},
{
name: "sort by created_time descending - verifies json_extract desc works",
sortBy: SortByCreatedTime,
sortDirection: Descending,
maxResultSetSize: 10,
expectedOrder: []string{"ttcp_3", "ttcp_2", "ttcp_1"}, // newest to oldest
expectIncomplete: false,
},
{
name: "sort ascending with limit - verifies LIMIT clause",
sortBy: SortByCreatedTime,
sortDirection: Ascending,
maxResultSetSize: 2,
expectedOrder: []string{"ttcp_1", "ttcp_2"},
expectIncomplete: true,
},
{
name: "sort descending with limit - verifies LIMIT clause",
sortBy: SortByCreatedTime,
sortDirection: Descending,
maxResultSetSize: 2,
expectedOrder: []string{"ttcp_3", "ttcp_2"},
expectIncomplete: true,
},
{
name: "no limit (-1)",
sortBy: SortByCreatedTime,
sortDirection: Ascending,
maxResultSetSize: -1,
expectedOrder: []string{"ttcp_1", "ttcp_2", "ttcp_3"},
expectIncomplete: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Build options
opts := []Option{
withAuthTokenId(at.Id),
WithMaxResultSetSize(tt.maxResultSetSize),
}
if tt.sortBy != SortByDefault {
opts = append(opts, WithSort(tt.sortBy, tt.sortDirection, []SortBy{SortByCreatedTime}))
}
// Execute the search
result, err := r.searchSessions(ctx, "true", nil, opts...)
require.NoError(t, err)
require.NotNil(t, result)
// Verify the result
if tt.expectIncomplete {
assert.True(t, result.Incomplete, "expected incomplete result")
assert.Len(t, result.Sessions, tt.maxResultSetSize, "expected limited results")
} else {
assert.False(t, result.Incomplete, "expected complete result")
}
// Verify the order if sorting was applied
// This proves json_extract(item, '$.created_time') is working correctly
if tt.sortBy != SortByDefault && len(tt.expectedOrder) > 0 {
var actualOrder []string
for _, sess := range result.Sessions {
actualOrder = append(actualOrder, sess.Id)
}
assert.Equal(t, tt.expectedOrder[:len(actualOrder)], actualOrder, "sessions not in expected order")
// Additionally verify the timestamps are actually in the correct order
if len(result.Sessions) > 1 {
for i := 0; i < len(result.Sessions)-1; i++ {
curr := result.Sessions[i].CreatedTime
next := result.Sessions[i+1].CreatedTime
if tt.sortDirection == Ascending {
assert.True(t, !curr.After(next), "timestamps should be in ascending order")
} else {
assert.True(t, !curr.Before(next), "timestamps should be in descending order")
}
}
}
}
})
}
}
func TestRepository_searchSessions_dbOptionsWithQuery(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create sessions with different created_time values
now := time.Now()
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "pending",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-2 * time.Hour),
},
{
Id: "ttcp_2",
Status: "active",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-1 * time.Hour),
},
{
Id: "ttcp_3",
Status: "active",
Endpoint: "address3",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now,
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
tests := []struct {
name string
query string
sortBy SortBy
sortDirection SortDirection
maxResultSetSize int
expectedCount int
expectedOrder []string
expectIncomplete bool
}{
{
name: "query with sort ascending",
query: `status % "active"`,
sortBy: SortByCreatedTime,
sortDirection: Ascending,
maxResultSetSize: 10,
expectedCount: 2,
expectedOrder: []string{"ttcp_2", "ttcp_3"},
expectIncomplete: false,
},
{
name: "query with sort descending",
query: `status % "active"`,
sortBy: SortByCreatedTime,
sortDirection: Descending,
maxResultSetSize: 10,
expectedCount: 2,
expectedOrder: []string{"ttcp_3", "ttcp_2"},
expectIncomplete: false,
},
{
name: "query with sort and limit",
query: `status % "active"`,
sortBy: SortByCreatedTime,
sortDirection: Descending,
maxResultSetSize: 1,
expectedCount: 1,
expectedOrder: []string{"ttcp_3"},
expectIncomplete: true,
},
{
name: "query without sort",
query: `status % "active"`,
sortBy: SortByDefault,
sortDirection: SortDirectionDefault,
maxResultSetSize: 10,
expectedCount: 2,
expectIncomplete: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Build options
opts := []Option{
WithMaxResultSetSize(tt.maxResultSetSize),
}
if tt.sortBy != SortByDefault {
opts = append(opts, WithSort(tt.sortBy, tt.sortDirection, []SortBy{SortByCreatedTime}))
}
// Execute the query
result, err := r.QuerySessions(ctx, at.Id, tt.query, opts...)
require.NoError(t, err)
require.NotNil(t, result)
// Verify results
assert.Len(t, result.Sessions, tt.expectedCount)
assert.Equal(t, tt.expectIncomplete, result.Incomplete)
// Verify order if expected
if len(tt.expectedOrder) > 0 {
var actualOrder []string
for _, sess := range result.Sessions {
actualOrder = append(actualOrder, sess.Id)
}
assert.Equal(t, tt.expectedOrder, actualOrder)
}
})
}
}
func TestRepository_searchSessions_limitBehavior(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create 10 sessions
var ss []*sessions.Session
for i := 0; i < 10; i++ {
ss = append(ss, &sessions.Session{
Id: fmt.Sprintf("ttcp_%d", i),
Status: "active",
Endpoint: fmt.Sprintf("address%d", i),
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
})
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
tests := []struct {
name string
maxResultSetSize int
expectedCount int
expectIncomplete bool
}{
{
name: "limit of 5",
maxResultSetSize: 5,
expectedCount: 5,
expectIncomplete: true,
},
{
name: "limit of 10 (exact match)",
maxResultSetSize: 10,
expectedCount: 10,
expectIncomplete: false,
},
{
name: "limit of 15 (more than available)",
maxResultSetSize: 15,
expectedCount: 10,
expectIncomplete: false,
},
{
name: "no limit (-1)",
maxResultSetSize: -1,
expectedCount: 10,
expectIncomplete: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Execute the list
result, err := r.ListSessions(ctx, at.Id, WithMaxResultSetSize(tt.maxResultSetSize))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, tt.expectedCount)
assert.Equal(t, tt.expectIncomplete, result.Incomplete)
})
}
}
func TestRepository_searchSessions_dbOptionsVerifyOrderClause(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create a single session
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "active",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
t.Run("verify order clause format for ascending", func(t *testing.T) {
result, err := r.ListSessions(ctx, at.Id,
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Ascending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
})
t.Run("verify order clause format for descending", func(t *testing.T) {
result, err := r.ListSessions(ctx, at.Id,
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Descending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
})
t.Run("verify no order clause when sortBy is default", func(t *testing.T) {
result, err := r.ListSessions(ctx, at.Id, WithMaxResultSetSize(10))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
})
}
func TestRepository_searchSessions_sortingEdgeCases(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create test sessions
now := time.Now()
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "active",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-1 * time.Hour),
},
{
Id: "ttcp_2",
Status: "pending",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now,
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
t.Run("unsupported sort field - SortByName not in sessionSortFieldMap", func(t *testing.T) {
// SortByName is not in sessionSortFieldMap, should return error
result, err := r.searchSessions(ctx, "true", nil,
withAuthTokenId(at.Id),
WithMaxResultSetSize(10),
WithSort(SortByName, Ascending, []SortBy{SortByName}))
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "unsupported sort field for sessions")
})
t.Run("invalid sort direction - unknown value", func(t *testing.T) {
// Using an invalid sort direction that's not in the switch statement
result, err := r.searchSessions(ctx, "true", nil,
withAuthTokenId(at.Id),
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, SortDirection("invalid"), []SortBy{SortByCreatedTime}))
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "unsupported sort direction")
})
t.Run("valid sort with SortDirectionDefault - should default to descending", func(t *testing.T) {
// SortDirectionDefault should be treated as ascending
result, err := r.searchSessions(ctx, "true", nil,
withAuthTokenId(at.Id),
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, SortDirectionDefault, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 2)
// Should be in descending order (newest first)
assert.Equal(t, "ttcp_2", result.Sessions[0].Id)
assert.Equal(t, "ttcp_1", result.Sessions[1].Id)
})
t.Run("sort with limit 1 - verify incomplete flag", func(t *testing.T) {
result, err := r.searchSessions(ctx, "true", nil,
withAuthTokenId(at.Id),
WithMaxResultSetSize(1),
WithSort(SortByCreatedTime, Descending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
assert.True(t, result.Incomplete)
assert.Equal(t, "ttcp_2", result.Sessions[0].Id) // newest first
})
t.Run("sort with no results - empty result set", func(t *testing.T) {
// Query that matches nothing
result, err := r.QuerySessions(ctx, at.Id, `status % "nonexistent"`,
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Ascending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 0)
assert.False(t, result.Incomplete)
})
}
func TestRepository_searchSessions_jsonExtractSafety(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create test session
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "active",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: time.Now(),
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
t.Run("verify json_extract uses safe field from sessionSortFieldMap", func(t *testing.T) {
// This test verifies that the json_extract uses the mapped field name
// from sessionSortFieldMap, which should be safe
result, err := r.searchSessions(ctx, "true", nil,
withAuthTokenId(at.Id),
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Ascending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
// If we got here without SQL errors, json_extract is working correctly
})
t.Run("verify sorting works with actual JSON data", func(t *testing.T) {
// Verify that the JSON item field contains valid data and sorting works
result, err := r.ListSessions(ctx, at.Id,
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Descending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
assert.Equal(t, "ttcp_1", result.Sessions[0].Id)
// Verify the session has all expected fields from JSON unmarshaling
assert.Equal(t, "active", result.Sessions[0].Status)
assert.Equal(t, "address1", result.Sessions[0].Endpoint)
})
}
func TestRepository_ListSessions_sortingValidation(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create test sessions
now := time.Now()
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "active",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-1 * time.Hour),
},
{
Id: "ttcp_2",
Status: "pending",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now,
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
t.Run("ListSessions with unsupported sort field", func(t *testing.T) {
// Trying to sort by a field not in sessionSortFieldMap
result, err := r.ListSessions(ctx, at.Id,
WithMaxResultSetSize(10),
WithSort(SortByName, Ascending, []SortBy{SortByName}))
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "unsupported sort field for sessions")
})
t.Run("ListSessions with valid sort field", func(t *testing.T) {
result, err := r.ListSessions(ctx, at.Id,
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Ascending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 2)
assert.Equal(t, "ttcp_1", result.Sessions[0].Id)
assert.Equal(t, "ttcp_2", result.Sessions[1].Id)
})
}
func TestRepository_QuerySessions_sortingValidation(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := &user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
// Create test sessions
now := time.Now()
ss := []*sessions.Session{
{
Id: "ttcp_1",
Status: "active",
Endpoint: "address1",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now.Add(-1 * time.Hour),
},
{
Id: "ttcp_2",
Status: "active",
Endpoint: "address2",
ScopeId: "p_123",
TargetId: "ttcp_123",
UserId: "u_123",
Type: "tcp",
CreatedTime: now,
},
}
require.NoError(t, r.refreshSessions(ctx, u, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{ss}, [][]string{nil})))))
t.Run("QuerySessions with unsupported sort field", func(t *testing.T) {
result, err := r.QuerySessions(ctx, at.Id, `status % "active"`,
WithMaxResultSetSize(10),
WithSort(SortByName, Ascending, []SortBy{SortByName}))
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "unsupported sort field for sessions")
})
t.Run("QuerySessions with valid sort field and query", func(t *testing.T) {
result, err := r.QuerySessions(ctx, at.Id, `status % "active"`,
WithMaxResultSetSize(10),
WithSort(SortByCreatedTime, Descending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 2)
// Descending order - newest first
assert.Equal(t, "ttcp_2", result.Sessions[0].Id)
assert.Equal(t, "ttcp_1", result.Sessions[1].Id)
})
t.Run("QuerySessions with sort and limit causing incomplete", func(t *testing.T) {
result, err := r.QuerySessions(ctx, at.Id, `status % "active"`,
WithMaxResultSetSize(1),
WithSort(SortByCreatedTime, Ascending, []SortBy{SortByCreatedTime}))
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.Sessions, 1)
assert.True(t, result.Incomplete)
assert.Equal(t, "ttcp_1", result.Sessions[0].Id) // oldest first
})
}
+3 -3
View File
@@ -28,7 +28,7 @@ const (
// Valid returns true if the SortBy value is a known good value
func (s SortBy) Valid() bool {
switch s {
case SortByDefault, SortByName, SortByCreatedAt:
case SortByDefault, SortByName, SortByCreatedTime:
return true
}
return false
@@ -154,7 +154,7 @@ func NewSearchService(ctx context.Context, repo *Repository) (*SearchService, er
}
in.Targets = finalResults
},
sortableColumns: []SortBy{SortByName, SortByCreatedAt},
sortableColumns: []SortBy{SortByName},
},
Sessions: &resourceSearchFns[*sessions.Session]{
list: repo.ListSessions,
@@ -168,7 +168,7 @@ func NewSearchService(ctx context.Context, repo *Repository) (*SearchService, er
}
in.Sessions = finalResults
},
sortableColumns: []SortBy{SortByCreatedAt},
sortableColumns: []SortBy{SortByCreatedTime},
},
ImplicitScopes: &resourceSearchFns[*scopes.Scope]{
list: repo.ListImplicitScopes,
+3 -3
View File
@@ -451,7 +451,7 @@ func TestSortByValid(t *testing.T) {
{SortByDefault, true},
{SortBy(""), true},
{SortByName, true},
{SortByCreatedAt, true},
{SortByCreatedTime, true},
{SortBy("unknown"), false},
{SortBy("id"), false},
{SortBy("invalid_column"), false},
@@ -594,11 +594,11 @@ func TestSearch_Sorting(t *testing.T) {
assert.Nil(t, got)
})
t.Run("sessions accept created_at sort", func(t *testing.T) {
t.Run("sessions accept created time sort", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: Sessions,
AuthTokenId: at.Id,
SortBy: SortByCreatedAt,
SortBy: SortByCreatedTime,
SortDirection: Descending,
})
require.NoError(t, err)