@@ -9,17 +9,23 @@ import (
99
1010 "github.com/codeGROOVE-dev/slacker/pkg/bot/cache"
1111 "github.com/codeGROOVE-dev/slacker/pkg/github"
12+ slackapi "github.com/codeGROOVE-dev/slacker/pkg/slack"
1213 "github.com/codeGROOVE-dev/slacker/pkg/state"
1314 "github.com/slack-go/slack"
1415)
1516
1617// mockStateStore implements StateStore interface from bot package.
18+ //
19+ //nolint:govet // fieldalignment optimization would reduce test readability
1720type mockStateStore struct {
1821 markProcessedErr error
1922 saveThreadErr error
23+ saveDMMessageErr error
2024 threads map [string ]cache.ThreadInfo
2125 dmTimes map [string ]time.Time
2226 dmUsers map [string ][]string
27+ dmMessages map [string ]state.DMInfo
28+ pendingDMs []* state.PendingDM
2329 processedEvents map [string ]bool
2430 lastNotifications map [string ]time.Time
2531 mu sync.Mutex
@@ -85,6 +91,48 @@ func (m *mockStateStore) ListDMUsers(ctx context.Context, prURL string) []string
8591 return []string {}
8692}
8793
94+ // DMMessage returns DM message info for a user and PR.
95+ func (m * mockStateStore ) DMMessage (ctx context.Context , userID , prURL string ) (state.DMInfo , bool ) {
96+ m .mu .Lock ()
97+ defer m .mu .Unlock ()
98+ key := userID + ":" + prURL
99+ if m .dmMessages != nil {
100+ if info , ok := m .dmMessages [key ]; ok {
101+ return info , true
102+ }
103+ }
104+ return state.DMInfo {}, false
105+ }
106+
107+ // SaveDMMessage saves DM message info for a user and PR.
108+ func (m * mockStateStore ) SaveDMMessage (ctx context.Context , userID , prURL string , info state.DMInfo ) error {
109+ m .mu .Lock ()
110+ defer m .mu .Unlock ()
111+ if m .saveDMMessageErr != nil {
112+ return m .saveDMMessageErr
113+ }
114+ key := userID + ":" + prURL
115+ if m .dmMessages == nil {
116+ m .dmMessages = make (map [string ]state.DMInfo )
117+ }
118+ m .dmMessages [key ] = info
119+ // Also track this user for ListDMUsers
120+ if m .dmUsers == nil {
121+ m .dmUsers = make (map [string ][]string )
122+ }
123+ found := false
124+ for _ , u := range m .dmUsers [prURL ] {
125+ if u == userID {
126+ found = true
127+ break
128+ }
129+ }
130+ if ! found {
131+ m .dmUsers [prURL ] = append (m .dmUsers [prURL ], userID )
132+ }
133+ return nil
134+ }
135+
88136func (m * mockStateStore ) WasProcessed (ctx context.Context , eventKey string ) bool {
89137 m .mu .Lock ()
90138 defer m .mu .Unlock ()
@@ -129,16 +177,35 @@ func (m *mockStateStore) RecordNotification(ctx context.Context, prURL string, n
129177}
130178
131179// QueuePendingDM implements notify.Store interface for DM queue management.
132- func (* mockStateStore ) QueuePendingDM (_ * state.PendingDM ) error {
133- return nil // No-op for tests
180+ func (m * mockStateStore ) QueuePendingDM (ctx context.Context , dm * state.PendingDM ) error {
181+ m .mu .Lock ()
182+ defer m .mu .Unlock ()
183+ m .pendingDMs = append (m .pendingDMs , dm )
184+ return nil
134185}
135186
136- func (* mockStateStore ) PendingDMs (_ time.Time ) ([]state.PendingDM , error ) {
137- return nil , nil // Return empty list for tests
187+ func (m * mockStateStore ) PendingDMs (ctx context.Context , before time.Time ) ([]state.PendingDM , error ) {
188+ m .mu .Lock ()
189+ defer m .mu .Unlock ()
190+ var result []state.PendingDM
191+ for _ , dm := range m .pendingDMs {
192+ if dm .SendAfter .Before (before ) {
193+ result = append (result , * dm )
194+ }
195+ }
196+ return result , nil
138197}
139198
140- func (* mockStateStore ) RemovePendingDM (_ string ) error {
141- return nil // No-op for tests
199+ func (m * mockStateStore ) RemovePendingDM (ctx context.Context , id string ) error {
200+ m .mu .Lock ()
201+ defer m .mu .Unlock ()
202+ for i , dm := range m .pendingDMs {
203+ if dm .ID == id {
204+ m .pendingDMs = append (m .pendingDMs [:i ], m .pendingDMs [i + 1 :]... )
205+ break
206+ }
207+ }
208+ return nil
142209}
143210
144211func (* mockStateStore ) Close () error {
@@ -149,26 +216,30 @@ func (*mockStateStore) Close() error {
149216//
150217//nolint:govet // fieldalignment optimization would reduce test readability
151218type mockSlackClient struct {
152- mu sync.Mutex
153- postThreadFunc func (ctx context.Context , channelID , text string , attachments []slack.Attachment ) (string , error )
154- updateMessageFunc func (ctx context.Context , channelID , timestamp , text string ) error
155- updateDMMessageFunc func (ctx context.Context , userID , timestamp , text string ) error
156- channelHistoryFunc func (ctx context.Context , channelID string , oldest , latest string , limit int ) (* slack.GetConversationHistoryResponse , error )
157- resolveChannelFunc func (ctx context.Context , channelName string ) string
158- botInChannelFunc func (ctx context.Context , channelID string ) bool
159- botInfoFunc func (ctx context.Context ) (* slack.AuthTestResponse , error )
160- workspaceInfoFunc func (ctx context.Context ) (* slack.TeamInfo , error )
161- publishHomeFunc func (ctx context.Context , userID string , blocks []slack.Block ) error
162- apiFunc func () * slack.Client
219+ mu sync.Mutex
220+ postThreadFunc func (ctx context.Context , channelID , text string , attachments []slack.Attachment ) (string , error )
221+ updateMessageFunc func (ctx context.Context , channelID , timestamp , text string ) error
222+ updateDMMessageFunc func (ctx context.Context , userID , timestamp , text string ) error
223+ sendDirectMessageFunc func (ctx context.Context , userID , text string ) (dmChannelID , messageTS string , err error )
224+ isUserInChannelFunc func (ctx context.Context , channelID , userID string ) bool
225+ findDMMessagesFunc func (ctx context.Context , userID , prURL string , since time.Time ) ([]slackapi.DMLocation , error )
226+ channelHistoryFunc func (ctx context.Context , channelID string , oldest , latest string , limit int ) (* slack.GetConversationHistoryResponse , error )
227+ resolveChannelFunc func (ctx context.Context , channelName string ) string
228+ botInChannelFunc func (ctx context.Context , channelID string ) bool
229+ botInfoFunc func (ctx context.Context ) (* slack.AuthTestResponse , error )
230+ workspaceInfoFunc func (ctx context.Context ) (* slack.TeamInfo , error )
231+ publishHomeFunc func (ctx context.Context , userID string , blocks []slack.Block ) error
232+ apiFunc func () * slack.Client
163233
164234 // For direct workspace info control
165235 workspaceInfo * slack.TeamInfo
166236 workspaceInfoErr bool
167237
168238 // Tracking for test assertions
169- postedMessages []mockPostedMessage
170- updatedMessages []mockUpdatedMessage
171- updatedDMMessage []mockUpdatedDMMessage
239+ postedMessages []mockPostedMessage
240+ updatedMessages []mockUpdatedMessage
241+ updatedDMMessage []mockUpdatedDMMessage
242+ sentDirectMessages []mockSentDirectMessage
172243}
173244
174245type mockPostedMessage struct {
@@ -189,6 +260,11 @@ type mockUpdatedDMMessage struct {
189260 Text string
190261}
191262
263+ type mockSentDirectMessage struct {
264+ UserID string
265+ Text string
266+ }
267+
192268func (m * mockSlackClient ) PostThread (ctx context.Context , channelID , text string , attachments []slack.Attachment ) (string , error ) {
193269 m .mu .Lock ()
194270 m .postedMessages = append (m .postedMessages , mockPostedMessage {
@@ -287,6 +363,37 @@ func (m *mockSlackClient) API() *slack.Client {
287363 return nil
288364}
289365
366+ // SendDirectMessage sends a DM to a user.
367+ func (m * mockSlackClient ) SendDirectMessage (ctx context.Context , userID , text string ) (dmChannelID , messageTS string , err error ) {
368+ m .mu .Lock ()
369+ m .sentDirectMessages = append (m .sentDirectMessages , mockSentDirectMessage {
370+ UserID : userID ,
371+ Text : text ,
372+ })
373+ m .mu .Unlock ()
374+ if m .sendDirectMessageFunc != nil {
375+ return m .sendDirectMessageFunc (ctx , userID , text )
376+ }
377+ return "D" + userID , "1234567890.123456" , nil
378+ }
379+
380+ // IsUserInChannel checks if a user is in a channel.
381+ func (m * mockSlackClient ) IsUserInChannel (ctx context.Context , channelID , userID string ) bool {
382+ if m .isUserInChannelFunc != nil {
383+ return m .isUserInChannelFunc (ctx , channelID , userID )
384+ }
385+ return false
386+ }
387+
388+ // FindDMMessagesInHistory searches DM history for messages containing a PR URL.
389+ func (m * mockSlackClient ) FindDMMessagesInHistory (ctx context.Context , userID , prURL string , since time.Time ) ([]slackapi.DMLocation , error ) {
390+ if m .findDMMessagesFunc != nil {
391+ return m .findDMMessagesFunc (ctx , userID , prURL , since )
392+ }
393+ // Default: return empty (no DMs found in history)
394+ return nil , nil
395+ }
396+
290397// mockUserMapper is a simple mock for user mapping in tests.
291398type mockUserMapper struct {
292399 slackHandleFunc func (ctx context.Context , githubUser , org , domain string ) (string , error )
0 commit comments