Skip to content

Commit b25aeba

Browse files
authored
feat(graphql): optimize userBatchFn to handle system users without API calls and add tests for system user scenarios (#114)
1 parent bc09fe8 commit b25aeba

2 files changed

Lines changed: 129 additions & 7 deletions

File tree

pkg/controller/graphql/dataloader.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,37 @@ func userBatchFn(slackClient interfaces.SlackClient) func(ctx context.Context, k
114114
return func(ctx context.Context, keys []string) []*dataloader.Result[*graphql1.User] {
115115
results := make([]*dataloader.Result[*graphql1.User], len(keys))
116116

117+
// Separate system users from regular Slack users
118+
var slackUserIDs []string
119+
slackUserIndices := make(map[int]string) // map[index]userID for Slack users
120+
121+
for i, key := range keys {
122+
if key == string(types.SystemUserID) {
123+
// Return system user immediately without calling Slack API
124+
results[i] = &dataloader.Result[*graphql1.User]{
125+
Data: &graphql1.User{ID: key, Name: "System"},
126+
Error: nil,
127+
}
128+
} else {
129+
slackUserIDs = append(slackUserIDs, key)
130+
slackUserIndices[i] = key
131+
}
132+
}
133+
134+
// If no Slack users to fetch, return early
135+
if len(slackUserIDs) == 0 {
136+
return results
137+
}
138+
117139
if slackClient != nil {
118140
// Use batch API to fetch all users at once
119-
slackUsers, err := slackClient.GetUsersInfo(keys...)
141+
slackUsers, err := slackClient.GetUsersInfo(slackUserIDs...)
120142
if err != nil {
121143
// Handle the error for debugging
122-
errutil.Handle(ctx, goerr.Wrap(err, "failed to get users info from Slack", goerr.V("userIDs", keys)))
144+
errutil.Handle(ctx, goerr.Wrap(err, "failed to get users info from Slack", goerr.V("userIDs", slackUserIDs)))
123145
// If Slack API fails with user_not_found or similar errors, fallback to ID instead of propagating error
124146
// This prevents the entire query from failing when some users don't exist in Slack
125-
for i, id := range keys {
147+
for i, id := range slackUserIndices {
126148
results[i] = &dataloader.Result[*graphql1.User]{
127149
Data: &graphql1.User{ID: id, Name: id},
128150
Error: nil,
@@ -163,13 +185,13 @@ func userBatchFn(slackClient interfaces.SlackClient) func(ctx context.Context, k
163185
}
164186

165187
// Build results in the same order as keys
166-
for i, key := range keys {
167-
if user, found := userMap[key]; found {
188+
for i, userID := range slackUserIndices {
189+
if user, found := userMap[userID]; found {
168190
results[i] = &dataloader.Result[*graphql1.User]{Data: user, Error: nil}
169191
} else {
170192
// User not found in Slack response, fallback to ID
171193
results[i] = &dataloader.Result[*graphql1.User]{
172-
Data: &graphql1.User{ID: key, Name: key},
194+
Data: &graphql1.User{ID: userID, Name: userID},
173195
Error: nil,
174196
}
175197
}
@@ -179,7 +201,7 @@ func userBatchFn(slackClient interfaces.SlackClient) func(ctx context.Context, k
179201
}
180202

181203
// Fallback for when SlackClient is nil (not an error condition)
182-
for i, id := range keys {
204+
for i, id := range slackUserIndices {
183205
results[i] = &dataloader.Result[*graphql1.User]{
184206
Data: &graphql1.User{ID: id, Name: id},
185207
Error: nil,

pkg/controller/graphql/dataloader_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,106 @@ func TestUserLoaderUseBatchMethod(t *testing.T) {
418418
}
419419
}
420420

421+
func TestSystemUserLoader(t *testing.T) {
422+
t.Run("System user does not call Slack API", func(t *testing.T) {
423+
repo, slackClient := setupTestData()
424+
loaders := NewDataLoaders(repo, slackClient)
425+
ctx := context.Background()
426+
427+
// Load system user
428+
user, err := GetUserWithLoaders(ctx, loaders, string(types.SystemUserID))
429+
gt.NoError(t, err)
430+
gt.Equal(t, user.ID, string(types.SystemUserID))
431+
gt.Equal(t, user.Name, "System")
432+
433+
// Verify Slack API was NOT called
434+
getUsersInfoCalls := slackClient.GetUsersInfoCalls()
435+
gt.Number(t, len(getUsersInfoCalls)).Equal(0)
436+
})
437+
438+
t.Run("System user mixed with regular users", func(t *testing.T) {
439+
repo, slackClient := setupTestData()
440+
loaders := NewDataLoaders(repo, slackClient)
441+
ctx := context.Background()
442+
443+
// Load system user and regular users concurrently
444+
userIDs := []string{string(types.SystemUserID), "user1", "user2"}
445+
results := make([]*graphql1.User, len(userIDs))
446+
errors := make([]error, len(userIDs))
447+
448+
var wg sync.WaitGroup
449+
for i, id := range userIDs {
450+
wg.Add(1)
451+
go func(index int, userID string) {
452+
defer wg.Done()
453+
u, err := GetUserWithLoaders(ctx, loaders, userID)
454+
results[index] = u
455+
errors[index] = err
456+
}(i, id)
457+
}
458+
wg.Wait()
459+
460+
// Verify all loads returned correct data
461+
for i, err := range errors {
462+
gt.NoError(t, err)
463+
gt.NotNil(t, results[i])
464+
gt.Equal(t, results[i].ID, userIDs[i])
465+
}
466+
467+
// Verify system user has correct name
468+
gt.Equal(t, results[0].Name, "System")
469+
gt.Equal(t, results[1].Name, "User One")
470+
gt.Equal(t, results[2].Name, "User Two")
471+
472+
// Verify Slack API was called only for non-system users
473+
getUsersInfoCalls := slackClient.GetUsersInfoCalls()
474+
gt.Number(t, len(getUsersInfoCalls)).Greater(0)
475+
476+
// The batch call should only contain non-system users
477+
if len(getUsersInfoCalls) > 0 {
478+
calledUsers := getUsersInfoCalls[0].Users
479+
for _, userID := range calledUsers {
480+
gt.NotEqual(t, userID, string(types.SystemUserID))
481+
}
482+
}
483+
})
484+
485+
t.Run("Multiple system users only", func(t *testing.T) {
486+
repo, slackClient := setupTestData()
487+
loaders := NewDataLoaders(repo, slackClient)
488+
ctx := context.Background()
489+
490+
// Load multiple system users
491+
userIDs := []string{string(types.SystemUserID), string(types.SystemUserID)}
492+
results := make([]*graphql1.User, len(userIDs))
493+
errors := make([]error, len(userIDs))
494+
495+
var wg sync.WaitGroup
496+
for i, id := range userIDs {
497+
wg.Add(1)
498+
go func(index int, userID string) {
499+
defer wg.Done()
500+
u, err := GetUserWithLoaders(ctx, loaders, userID)
501+
results[index] = u
502+
errors[index] = err
503+
}(i, id)
504+
}
505+
wg.Wait()
506+
507+
// Verify all loads returned correct data
508+
for i, err := range errors {
509+
gt.NoError(t, err)
510+
gt.NotNil(t, results[i])
511+
gt.Equal(t, results[i].ID, string(types.SystemUserID))
512+
gt.Equal(t, results[i].Name, "System")
513+
}
514+
515+
// Verify Slack API was NOT called at all
516+
getUsersInfoCalls := slackClient.GetUsersInfoCalls()
517+
gt.Number(t, len(getUsersInfoCalls)).Equal(0)
518+
})
519+
}
520+
421521
// Helper functions for testing with loaders
422522
func GetTicketWithLoaders(ctx context.Context, loaders *DataLoaders, ticketID types.TicketID) (*ticket.Ticket, error) {
423523
thunk := loaders.TicketLoader.Load(ctx, ticketID)

0 commit comments

Comments
 (0)