Skip to content

Commit 4fb5c65

Browse files
committed
test: Add comprehensive tests for QueryResourcesCount endpoint
Add complete test coverage for the new resource count functionality: - Service layer tests for QueryResourcesCount endpoint with various scenarios - OpenSearch implementation tests with mock client for count and aggregation - Mock resource searcher tests covering count operations and filters - Enhanced mock implementations with test helper methods - Service struct verification for new resourceCountService field Tests cover success scenarios, error handling, access control integration, and all query parameters (name, type, tags, parent filters). Generated with Claude Code (https://claude.ai/code) Signed-off-by: Andres Tobon <[email protected]>
1 parent 75688bc commit 4fb5c65

File tree

7 files changed

+928
-5
lines changed

7 files changed

+928
-5
lines changed

cmd/service/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (s *querySvcsrvc) QueryResources(ctx context.Context, p *querysvc.QueryReso
7272
// relationship.
7373
func (s *querySvcsrvc) QueryResourcesCount(ctx context.Context, p *querysvc.QueryResourcesCountPayload) (*querysvc.QueryResourcesCountResult, error) {
7474

75-
slog.DebugContext(ctx, "querySvc.query-resources",
75+
slog.DebugContext(ctx, "querySvc.query-resource-counts",
7676
"name", p.Name,
7777
)
7878

cmd/service/service_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package service
55

66
import (
77
"context"
8+
"fmt"
89
"testing"
910

1011
querysvc "github.com/linuxfoundation/lfx-v2-query-service/gen/query_svc"
@@ -172,6 +173,129 @@ func TestQuerySvcsrvc_QueryResources(t *testing.T) {
172173
}
173174
}
174175

176+
func TestQuerySvcsrvc_QueryResourcesCount(t *testing.T) {
177+
tests := []struct {
178+
name string
179+
payload *querysvc.QueryResourcesCountPayload
180+
setupMocks func(*mock.MockResourceSearcher, *mock.MockAccessControlChecker)
181+
expectedError bool
182+
expectedErrorType interface{}
183+
expectedCount uint64
184+
}{
185+
{
186+
name: "successful count query",
187+
payload: &querysvc.QueryResourcesCountPayload{
188+
Version: "1",
189+
Type: stringPtr("project"),
190+
},
191+
setupMocks: func(searcher *mock.MockResourceSearcher, accessChecker *mock.MockAccessControlChecker) {
192+
searcher.SetQueryResourcesCountResponse(&model.CountResult{
193+
Count: 5,
194+
HasMore: false,
195+
})
196+
accessChecker.DefaultResult = "allowed"
197+
},
198+
expectedError: false,
199+
expectedCount: 5,
200+
},
201+
{
202+
name: "successful count query with name filter",
203+
payload: &querysvc.QueryResourcesCountPayload{
204+
Version: "1",
205+
Name: stringPtr("Test"),
206+
Type: stringPtr("committee"),
207+
},
208+
setupMocks: func(searcher *mock.MockResourceSearcher, accessChecker *mock.MockAccessControlChecker) {
209+
searcher.SetQueryResourcesCountResponse(&model.CountResult{
210+
Count: 2,
211+
HasMore: false,
212+
})
213+
accessChecker.DefaultResult = "allowed"
214+
},
215+
expectedError: false,
216+
expectedCount: 2,
217+
},
218+
{
219+
name: "count query with tags",
220+
payload: &querysvc.QueryResourcesCountPayload{
221+
Version: "1",
222+
Tags: []string{"active", "governance"},
223+
},
224+
setupMocks: func(searcher *mock.MockResourceSearcher, accessChecker *mock.MockAccessControlChecker) {
225+
searcher.SetQueryResourcesCountResponse(&model.CountResult{
226+
Count: 10,
227+
HasMore: true,
228+
})
229+
accessChecker.DefaultResult = "allowed"
230+
},
231+
expectedError: false,
232+
expectedCount: 10,
233+
},
234+
{
235+
name: "count query with parent filter",
236+
payload: &querysvc.QueryResourcesCountPayload{
237+
Version: "1",
238+
Parent: stringPtr("project:123"),
239+
},
240+
setupMocks: func(searcher *mock.MockResourceSearcher, accessChecker *mock.MockAccessControlChecker) {
241+
searcher.SetQueryResourcesCountResponse(&model.CountResult{
242+
Count: 3,
243+
HasMore: false,
244+
})
245+
accessChecker.DefaultResult = "allowed"
246+
},
247+
expectedError: false,
248+
expectedCount: 3,
249+
},
250+
{
251+
name: "count query with service error",
252+
payload: &querysvc.QueryResourcesCountPayload{
253+
Version: "1",
254+
Type: stringPtr("invalid"),
255+
},
256+
setupMocks: func(searcher *mock.MockResourceSearcher, accessChecker *mock.MockAccessControlChecker) {
257+
searcher.SetQueryResourcesCountError(fmt.Errorf("service error"))
258+
},
259+
expectedError: true,
260+
expectedErrorType: &querysvc.InternalServerError{},
261+
},
262+
}
263+
264+
for _, tc := range tests {
265+
t.Run(tc.name, func(t *testing.T) {
266+
// Setup mocks
267+
mockResourceSearcher := mock.NewMockResourceSearcher()
268+
mockAccessChecker := mock.NewMockAccessControlChecker()
269+
mockOrgSearcher := mock.NewMockOrganizationSearcher()
270+
tc.setupMocks(mockResourceSearcher, mockAccessChecker)
271+
272+
service := NewQuerySvc(mockResourceSearcher, mockAccessChecker, mockOrgSearcher, mock.NewMockAuthService())
273+
svc, ok := service.(*querySvcsrvc)
274+
assert.True(t, ok)
275+
276+
ctx := context.WithValue(context.Background(), constants.PrincipalContextID, "test-user")
277+
278+
// Execute
279+
result, err := svc.QueryResourcesCount(ctx, tc.payload)
280+
281+
// Verify
282+
if tc.expectedError {
283+
assert.Error(t, err)
284+
if tc.expectedErrorType != nil {
285+
assert.IsType(t, tc.expectedErrorType, err)
286+
}
287+
assert.Nil(t, result)
288+
} else {
289+
assert.NoError(t, err)
290+
assert.NotNil(t, result)
291+
assert.Equal(t, tc.expectedCount, result.Count)
292+
// HasMore is returned from the service
293+
assert.NotNil(t, result.HasMore)
294+
}
295+
})
296+
}
297+
}
298+
175299
func TestQuerySvcsrvc_QueryOrgs(t *testing.T) {
176300
tests := []struct {
177301
name string
@@ -454,6 +578,7 @@ func TestNewQuerySvc(t *testing.T) {
454578
// Cast to concrete type to verify internal fields
455579
if svc, ok := result.(*querySvcsrvc); ok {
456580
assert.NotNil(t, svc.resourceService)
581+
assert.NotNil(t, svc.resourceCountService)
457582
assert.NotNil(t, svc.organizationService)
458583
}
459584
} else {

internal/infrastructure/mock/access_control.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ type MockAccessControlChecker struct {
2525
SimulateErrors bool
2626
// DefaultResult is the default access result ("allowed" or "denied")
2727
DefaultResult string
28+
// Test helper fields
29+
checkAccessResponse map[string]string
30+
checkAccessError error
31+
isReadyError error
2832
}
2933

3034
// CheckAccess implements the AccessControlChecker interface with mock behavior
@@ -36,6 +40,16 @@ func (m *MockAccessControlChecker) CheckAccess(ctx context.Context, subj string,
3640
"public_only", m.PublicResourcesOnly,
3741
)
3842

43+
// If test has set a mock error, return it
44+
if m.checkAccessError != nil {
45+
return nil, m.checkAccessError
46+
}
47+
48+
// If test has set a mock response, return it
49+
if m.checkAccessResponse != nil {
50+
return m.checkAccessResponse, nil
51+
}
52+
3953
result := make(model.AccessCheckResult)
4054

4155
// Parse the input data - expecting line-separated permission requests
@@ -85,6 +99,9 @@ func (m *MockAccessControlChecker) Close() error {
8599

86100
// IsReady implements the AccessControlChecker interface (always ready for mock)
87101
func (m *MockAccessControlChecker) IsReady(ctx context.Context) error {
102+
if m.isReadyError != nil {
103+
return m.isReadyError
104+
}
88105
return nil
89106
}
90107

@@ -171,3 +188,20 @@ func NewMockAccessControlCheckerDenyAll() *MockAccessControlChecker {
171188
DefaultResult: "denied",
172189
}
173190
}
191+
192+
// Test helper methods for setting up mock responses
193+
194+
// SetCheckAccessResponse sets the mock response for CheckAccess calls
195+
func (m *MockAccessControlChecker) SetCheckAccessResponse(response map[string]string) {
196+
m.checkAccessResponse = response
197+
}
198+
199+
// SetCheckAccessError sets the mock error for CheckAccess calls
200+
func (m *MockAccessControlChecker) SetCheckAccessError(err error) {
201+
m.checkAccessError = err
202+
}
203+
204+
// SetIsReadyError sets the mock error for IsReady calls
205+
func (m *MockAccessControlChecker) SetIsReadyError(err error) {
206+
m.isReadyError = err
207+
}

internal/infrastructure/mock/resource_searcher.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import (
1414
// MockResourceSearcher is a mock implementation of ResourceSearcher for testing
1515
// This demonstrates how the clean architecture allows easy swapping of implementations
1616
type MockResourceSearcher struct {
17-
resources []model.Resource
17+
resources []model.Resource
18+
queryResourcesCountResponse *model.CountResult
19+
queryResourcesCountError error
20+
isReadyError error
1821
}
1922

2023
// NewMockResourceSearcher creates a new mock searcher with some sample data
@@ -219,6 +222,16 @@ func (m *MockResourceSearcher) QueryResources(ctx context.Context, criteria mode
219222
func (m *MockResourceSearcher) QueryResourcesCount(ctx context.Context, countCriteria model.SearchCriteria, aggregationCriteria model.SearchCriteria, publicOnly bool) (*model.CountResult, error) {
220223
slog.DebugContext(ctx, "executing mock count search", "countCriteria", countCriteria, "aggregationCriteria", aggregationCriteria, "publicOnly", publicOnly)
221224

225+
// If test has set a mock error, return it
226+
if m.queryResourcesCountError != nil {
227+
return nil, m.queryResourcesCountError
228+
}
229+
230+
// If test has set a mock response, return it
231+
if m.queryResourcesCountResponse != nil {
232+
return m.queryResourcesCountResponse, nil
233+
}
234+
222235
// Filter resources based on countCriteria
223236
var filteredResources []model.Resource
224237

@@ -331,6 +344,9 @@ func (m *MockResourceSearcher) QueryResourcesCount(ctx context.Context, countCri
331344

332345
// IsReady implements the ResourceSearcher interface (always ready for mock)
333346
func (m *MockResourceSearcher) IsReady(ctx context.Context) error {
347+
if m.isReadyError != nil {
348+
return m.isReadyError
349+
}
334350
return nil
335351
}
336352

@@ -428,3 +444,20 @@ func (m *MockResourceSearcher) ClearResources() {
428444
func (m *MockResourceSearcher) GetResourceCount() int {
429445
return len(m.resources)
430446
}
447+
448+
// Test helper methods for setting up mock responses
449+
450+
// SetQueryResourcesCountResponse sets the mock response for QueryResourcesCount calls
451+
func (m *MockResourceSearcher) SetQueryResourcesCountResponse(response *model.CountResult) {
452+
m.queryResourcesCountResponse = response
453+
}
454+
455+
// SetQueryResourcesCountError sets the mock error for QueryResourcesCount calls
456+
func (m *MockResourceSearcher) SetQueryResourcesCountError(err error) {
457+
m.queryResourcesCountError = err
458+
}
459+
460+
// SetIsReadyError sets the mock error for IsReady calls
461+
func (m *MockResourceSearcher) SetIsReadyError(err error) {
462+
m.isReadyError = err
463+
}

0 commit comments

Comments
 (0)