Skip to content

Commit 619399c

Browse files
authored
Merge pull request #62 from codeGROOVE-dev/reliable
more integration/unit testing
2 parents 0fb814c + 636ae9f commit 619399c

File tree

12 files changed

+3813
-0
lines changed

12 files changed

+3813
-0
lines changed

internal/bot/integration_test.go

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
package bot
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
ghmailto "github.com/codeGROOVE-dev/gh-mailto/pkg/gh-mailto"
10+
"github.com/codeGROOVE-dev/slacker/internal/notify"
11+
"github.com/codeGROOVE-dev/slacker/internal/slack"
12+
"github.com/codeGROOVE-dev/slacker/internal/slacktest"
13+
"github.com/codeGROOVE-dev/slacker/internal/usermapping"
14+
"github.com/codeGROOVE-dev/turnclient/pkg/turn"
15+
slackapi "github.com/slack-go/slack"
16+
)
17+
18+
// TestUserMappingIntegration tests the complete flow of mapping GitHub users to Slack users.
19+
func TestUserMappingIntegration(t *testing.T) {
20+
ctx := context.Background()
21+
22+
// Setup mock Slack server
23+
mockSlack := slacktest.New()
24+
defer mockSlack.Close()
25+
26+
// Add test users
27+
mockSlack.AddUser("[email protected]", "U001", "alice")
28+
mockSlack.AddUser("[email protected]", "U002", "bob")
29+
30+
// Create Slack client pointing to mock server
31+
slackClient := slackapi.New("test-token", slackapi.OptionAPIURL(mockSlack.URL+"/api/"))
32+
33+
// Create mock GitHub email lookup
34+
mockGitHub := &mockGitHubLookup{
35+
emails: map[string][]string{
36+
"alice": {"[email protected]"},
37+
38+
"charlie": {"[email protected]"}, // Won't be found in Slack
39+
},
40+
}
41+
42+
// Create user mapper
43+
userMapper := usermapping.NewForTesting(slackClient, mockGitHub)
44+
45+
tests := []struct {
46+
name string
47+
githubUsername string
48+
wantSlackID string
49+
wantErr bool
50+
}{
51+
{
52+
name: "successful mapping with single email",
53+
githubUsername: "alice",
54+
wantSlackID: "U001",
55+
wantErr: false,
56+
},
57+
{
58+
name: "successful mapping with multiple emails",
59+
githubUsername: "bob",
60+
wantSlackID: "U002",
61+
wantErr: false,
62+
},
63+
{
64+
name: "no mapping - email not in Slack",
65+
githubUsername: "charlie",
66+
wantSlackID: "",
67+
wantErr: false, // Not an error, just no mapping
68+
},
69+
{
70+
name: "no mapping - no GitHub emails found",
71+
githubUsername: "unknown",
72+
wantSlackID: "",
73+
wantErr: false,
74+
},
75+
}
76+
77+
for _, tt := range tests {
78+
t.Run(tt.name, func(t *testing.T) {
79+
slackID, err := userMapper.SlackHandle(ctx, tt.githubUsername, "test-org", "example.com")
80+
81+
if (err != nil) != tt.wantErr {
82+
t.Errorf("SlackHandle() error = %v, wantErr %v", err, tt.wantErr)
83+
}
84+
85+
if slackID != tt.wantSlackID {
86+
t.Errorf("SlackHandle() = %v, want %v", slackID, tt.wantSlackID)
87+
}
88+
})
89+
}
90+
91+
// Verify email lookups were performed
92+
emailLookups := mockSlack.GetEmailLookups()
93+
if len(emailLookups) < 3 {
94+
t.Errorf("Expected at least 3 email lookups, got %d", len(emailLookups))
95+
}
96+
}
97+
98+
// TestTurnclientEmojiIntegration tests the complete emoji determination pipeline.
99+
func TestTurnclientEmojiIntegration(t *testing.T) {
100+
tests := []struct {
101+
name string
102+
workflowState string
103+
nextActions map[string]turn.Action
104+
expectedEmoji string
105+
expectedAction string
106+
description string
107+
}{
108+
{
109+
name: "newly published takes precedence",
110+
workflowState: "newly_published",
111+
nextActions: map[string]turn.Action{
112+
"author": {Kind: turn.ActionFixTests},
113+
},
114+
expectedEmoji: ":new:",
115+
expectedAction: "fix_tests",
116+
description: "newly_published state should always show :new: even with other actions",
117+
},
118+
{
119+
name: "draft needs publishing",
120+
workflowState: "awaiting_action",
121+
nextActions: map[string]turn.Action{
122+
"author": {Kind: turn.ActionPublishDraft},
123+
},
124+
expectedEmoji: ":construction:",
125+
expectedAction: "publish_draft",
126+
description: "unpublished draft should show construction emoji",
127+
},
128+
{
129+
name: "tests broken has highest action priority",
130+
workflowState: "blocked",
131+
nextActions: map[string]turn.Action{
132+
"author": {Kind: turn.ActionFixTests},
133+
"reviewer": {Kind: turn.ActionReview},
134+
},
135+
expectedEmoji: ":cockroach:",
136+
expectedAction: "fix_tests",
137+
description: "fix_tests should take priority over review",
138+
},
139+
{
140+
name: "awaiting review",
141+
workflowState: "awaiting_review",
142+
nextActions: map[string]turn.Action{
143+
"reviewer": {Kind: turn.ActionReview},
144+
},
145+
expectedEmoji: ":hourglass:",
146+
expectedAction: "review",
147+
description: "review action should show hourglass",
148+
},
149+
{
150+
name: "ready to merge",
151+
workflowState: "approved",
152+
nextActions: map[string]turn.Action{
153+
"author": {Kind: turn.ActionMerge},
154+
},
155+
expectedEmoji: ":rocket:",
156+
expectedAction: "merge",
157+
description: "merge action should show rocket",
158+
},
159+
{
160+
name: "merged with no actions shows fallback",
161+
workflowState: "merged",
162+
nextActions: map[string]turn.Action{},
163+
expectedEmoji: ":postal_horn:",
164+
expectedAction: "",
165+
description: "This is a known bug - merged state with no actions falls back to postal_horn",
166+
},
167+
{
168+
name: "multiple reviewers prioritized correctly",
169+
workflowState: "awaiting_review",
170+
nextActions: map[string]turn.Action{
171+
"alice": {Kind: turn.ActionReview},
172+
"bob": {Kind: turn.ActionApprove},
173+
"carol": {Kind: turn.ActionReview},
174+
},
175+
expectedEmoji: ":hourglass:",
176+
expectedAction: "review",
177+
description: "review has lower priority number (6) than approve (7)",
178+
},
179+
}
180+
181+
for _, tt := range tests {
182+
t.Run(tt.name, func(t *testing.T) {
183+
// Test emoji determination
184+
emoji := notify.PrefixForAnalysis(tt.workflowState, tt.nextActions)
185+
if emoji != tt.expectedEmoji {
186+
t.Errorf("%s: PrefixForAnalysis() = %q, want %q",
187+
tt.description, emoji, tt.expectedEmoji)
188+
}
189+
190+
// Test primary action extraction
191+
primaryAction := notify.PrimaryAction(tt.nextActions)
192+
if primaryAction != tt.expectedAction {
193+
t.Errorf("%s: PrimaryAction() = %q, want %q",
194+
tt.description, primaryAction, tt.expectedAction)
195+
}
196+
197+
// Verify consistency: if there's a primary action, its emoji should match
198+
// (except for newly_published and merged edge cases)
199+
if tt.workflowState != "newly_published" && tt.workflowState != "merged" && tt.expectedAction != "" {
200+
actionEmoji := notify.PrefixForAction(tt.expectedAction)
201+
if emoji != actionEmoji {
202+
t.Errorf("%s: emoji mismatch - PrefixForAnalysis=%q but PrefixForAction(%q)=%q",
203+
tt.description, emoji, tt.expectedAction, actionEmoji)
204+
}
205+
}
206+
})
207+
}
208+
}
209+
210+
// TestDMDelayLogicIntegration tests the DM delay decision logic.
211+
func TestDMDelayLogicIntegration(t *testing.T) {
212+
ctx := context.Background()
213+
214+
// Setup mock Slack server
215+
mockSlack := slacktest.New()
216+
defer mockSlack.Close()
217+
218+
mockSlack.AddUser("[email protected]", "U001", "alice")
219+
mockSlack.AddChannel("C123", "dev", true)
220+
mockSlack.AddChannelMember("C123", "U001") // alice is in channel
221+
mockSlack.AddChannel("C456", "other", true)
222+
// alice is NOT in C456
223+
224+
slackClient := slackapi.New("test-token", slackapi.OptionAPIURL(mockSlack.URL+"/api/"))
225+
226+
// Create notification manager
227+
slackManager := slack.NewManager("")
228+
slackManager.RegisterWorkspace(ctx, "test-workspace", slackClient)
229+
230+
configMgr := &mockConfigManager{
231+
dmDelay: 65, // 65 minute delay
232+
}
233+
234+
notifier := notify.New(slackManager, configMgr)
235+
236+
prInfo := notify.PRInfo{
237+
Owner: "test",
238+
Repo: "repo",
239+
Number: 1,
240+
Title: "Test PR",
241+
Author: "author",
242+
State: "awaiting_review",
243+
HTMLURL: "https://github.com/test/repo/pull/1",
244+
WorkflowState: "awaiting_review",
245+
NextAction: map[string]turn.Action{
246+
"alice": {Kind: turn.ActionReview},
247+
},
248+
}
249+
250+
tests := []struct {
251+
name string
252+
setupFunc func()
253+
channelID string
254+
expectDM bool
255+
description string
256+
}{
257+
{
258+
name: "user in channel - delay should apply",
259+
setupFunc: func() {
260+
notifier.Tracker.UpdateUserPRChannelTag("test-workspace", "U001", "C123", "test", "repo", 1)
261+
},
262+
channelID: "C123",
263+
expectDM: false,
264+
description: "User is in channel where tagged - DM should be delayed",
265+
},
266+
{
267+
name: "user not in channel - immediate DM",
268+
setupFunc: func() {
269+
notifier.Tracker.UpdateUserPRChannelTag("test-workspace", "U001", "C456", "test", "repo", 1)
270+
},
271+
channelID: "C456",
272+
expectDM: true,
273+
description: "User is NOT in channel where tagged - DM should be immediate",
274+
},
275+
{
276+
name: "no channel tag - immediate DM",
277+
setupFunc: func() {
278+
// Don't record any channel tag
279+
},
280+
channelID: "",
281+
expectDM: true,
282+
description: "No channel tag recorded - DM should be immediate",
283+
},
284+
}
285+
286+
for _, tt := range tests {
287+
t.Run(tt.name, func(t *testing.T) {
288+
// Reset state
289+
mockSlack.Reset()
290+
291+
// Create fresh tracker with initialized maps
292+
notifier.Tracker = &notify.NotificationTracker{}
293+
// Initialize the tracker by creating a new notifier
294+
notifier = notify.New(slackManager, configMgr)
295+
296+
// Setup test scenario
297+
if tt.setupFunc != nil {
298+
tt.setupFunc()
299+
}
300+
301+
// Execute
302+
err := notifier.NotifyUser(ctx, "test-workspace", "U001", tt.channelID, "dev", prInfo)
303+
if err != nil {
304+
t.Errorf("NotifyUser() error = %v", err)
305+
}
306+
307+
// Give async operations time to complete
308+
time.Sleep(50 * time.Millisecond)
309+
310+
// Verify
311+
postedMessages := mockSlack.GetPostedMessages()
312+
dmCount := 0
313+
for _, msg := range postedMessages {
314+
if strings.HasPrefix(msg.Channel, "D") {
315+
dmCount++
316+
}
317+
}
318+
319+
if tt.expectDM && dmCount == 0 {
320+
t.Errorf("%s: expected DM to be sent, but none were sent", tt.description)
321+
}
322+
323+
if !tt.expectDM && dmCount > 0 {
324+
t.Errorf("%s: expected no DM (delay should apply), but %d DMs were sent", tt.description, dmCount)
325+
}
326+
})
327+
}
328+
}
329+
330+
// Mock implementations for testing
331+
332+
type mockGitHubLookup struct {
333+
emails map[string][]string
334+
}
335+
336+
func (m *mockGitHubLookup) Lookup(ctx context.Context, username, organization string) (*ghmailto.Result, error) {
337+
emails, exists := m.emails[username]
338+
if !exists {
339+
return &ghmailto.Result{
340+
Username: username,
341+
Addresses: []ghmailto.Address{},
342+
}, nil
343+
}
344+
345+
addresses := make([]ghmailto.Address, len(emails))
346+
for i, email := range emails {
347+
addresses[i] = ghmailto.Address{
348+
Email: email,
349+
Methods: []string{"Test Mock"},
350+
}
351+
}
352+
353+
return &ghmailto.Result{
354+
Username: username,
355+
Addresses: addresses,
356+
}, nil
357+
}
358+
359+
func (m *mockGitHubLookup) Guess(ctx context.Context, username, organization string, opts ghmailto.GuessOptions) (*ghmailto.GuessResult, error) {
360+
return &ghmailto.GuessResult{
361+
Username: username,
362+
Guesses: []ghmailto.Address{},
363+
}, nil
364+
}
365+
366+
type mockConfigManager struct {
367+
dmDelay int
368+
}
369+
370+
func (m *mockConfigManager) DailyRemindersEnabled(org string) bool {
371+
return false
372+
}
373+
374+
func (m *mockConfigManager) ReminderDMDelay(org, channel string) int {
375+
return m.dmDelay
376+
}

0 commit comments

Comments
 (0)