44 "context"
55 "errors"
66 "fmt"
7+ "sync"
78 "time"
89
910 ghmailto "github.com/codeGROOVE-dev/gh-mailto/pkg/gh-mailto"
@@ -15,6 +16,7 @@ import (
1516
1617// mockStateStore implements StateStore interface from bot package.
1718type mockStateStore struct {
19+ mu sync.Mutex
1820 threads map [string ]ThreadInfo
1921 dmTimes map [string ]time.Time
2022 dmUsers map [string ][]string
@@ -25,6 +27,8 @@ type mockStateStore struct {
2527}
2628
2729func (m * mockStateStore ) Thread (owner , repo string , number int , channelID string ) (ThreadInfo , bool ) {
30+ m .mu .Lock ()
31+ defer m .mu .Unlock ()
2832 key := fmt .Sprintf ("%s/%s#%d:%s" , owner , repo , number , channelID )
2933 if m .threads != nil {
3034 if info , ok := m .threads [key ]; ok {
@@ -35,6 +39,8 @@ func (m *mockStateStore) Thread(owner, repo string, number int, channelID string
3539}
3640
3741func (m * mockStateStore ) SaveThread (owner , repo string , number int , channelID string , info ThreadInfo ) error {
42+ m .mu .Lock ()
43+ defer m .mu .Unlock ()
3844 if m .saveThreadErr != nil {
3945 return m .saveThreadErr
4046 }
@@ -47,6 +53,8 @@ func (m *mockStateStore) SaveThread(owner, repo string, number int, channelID st
4753}
4854
4955func (m * mockStateStore ) LastDM (userID , prURL string ) (time.Time , bool ) {
56+ m .mu .Lock ()
57+ defer m .mu .Unlock ()
5058 key := userID + ":" + prURL
5159 if m .dmTimes != nil {
5260 if t , ok := m .dmTimes [key ]; ok {
@@ -57,6 +65,8 @@ func (m *mockStateStore) LastDM(userID, prURL string) (time.Time, bool) {
5765}
5866
5967func (m * mockStateStore ) RecordDM (userID , prURL string , sentAt time.Time ) error {
68+ m .mu .Lock ()
69+ defer m .mu .Unlock ()
6070 key := userID + ":" + prURL
6171 if m .dmTimes == nil {
6272 m .dmTimes = make (map [string ]time.Time )
@@ -66,6 +76,8 @@ func (m *mockStateStore) RecordDM(userID, prURL string, sentAt time.Time) error
6676}
6777
6878func (m * mockStateStore ) ListDMUsers (prURL string ) []string {
79+ m .mu .Lock ()
80+ defer m .mu .Unlock ()
6981 if m .dmUsers != nil {
7082 if users , ok := m .dmUsers [prURL ]; ok {
7183 return users
@@ -75,13 +87,17 @@ func (m *mockStateStore) ListDMUsers(prURL string) []string {
7587}
7688
7789func (m * mockStateStore ) WasProcessed (eventKey string ) bool {
90+ m .mu .Lock ()
91+ defer m .mu .Unlock ()
7892 if m .processedEvents != nil {
7993 return m .processedEvents [eventKey ]
8094 }
8195 return false
8296}
8397
8498func (m * mockStateStore ) MarkProcessed (eventKey string , _ time.Duration ) error {
99+ m .mu .Lock ()
100+ defer m .mu .Unlock ()
85101 if m .markProcessedErr != nil {
86102 return m .markProcessedErr
87103 }
@@ -93,6 +109,8 @@ func (m *mockStateStore) MarkProcessed(eventKey string, _ time.Duration) error {
93109}
94110
95111func (m * mockStateStore ) LastNotification (prURL string ) time.Time {
112+ m .mu .Lock ()
113+ defer m .mu .Unlock ()
96114 if m .lastNotifications != nil {
97115 if t , ok := m .lastNotifications [prURL ]; ok {
98116 return t
@@ -102,6 +120,8 @@ func (m *mockStateStore) LastNotification(prURL string) time.Time {
102120}
103121
104122func (m * mockStateStore ) RecordNotification (prURL string , notifiedAt time.Time ) error {
123+ m .mu .Lock ()
124+ defer m .mu .Unlock ()
105125 if m .lastNotifications == nil {
106126 m .lastNotifications = make (map [string ]time.Time )
107127 }
@@ -130,6 +150,7 @@ func (*mockStateStore) Close() error {
130150//
131151//nolint:govet // fieldalignment optimization would reduce test readability
132152type mockSlackClient struct {
153+ mu sync.Mutex
133154 postThreadFunc func (ctx context.Context , channelID , text string , attachments []slack.Attachment ) (string , error )
134155 updateMessageFunc func (ctx context.Context , channelID , timestamp , text string ) error
135156 updateDMMessageFunc func (ctx context.Context , userID , timestamp , text string ) error
@@ -170,35 +191,41 @@ type mockUpdatedDMMessage struct {
170191}
171192
172193func (m * mockSlackClient ) PostThread (ctx context.Context , channelID , text string , attachments []slack.Attachment ) (string , error ) {
194+ m .mu .Lock ()
173195 m .postedMessages = append (m .postedMessages , mockPostedMessage {
174196 ChannelID : channelID ,
175197 Text : text ,
176198 Attachments : attachments ,
177199 })
200+ m .mu .Unlock ()
178201 if m .postThreadFunc != nil {
179202 return m .postThreadFunc (ctx , channelID , text , attachments )
180203 }
181204 return "1234567890.123456" , nil
182205}
183206
184207func (m * mockSlackClient ) UpdateMessage (ctx context.Context , channelID , timestamp , text string ) error {
208+ m .mu .Lock ()
185209 m .updatedMessages = append (m .updatedMessages , mockUpdatedMessage {
186210 ChannelID : channelID ,
187211 Timestamp : timestamp ,
188212 Text : text ,
189213 })
214+ m .mu .Unlock ()
190215 if m .updateMessageFunc != nil {
191216 return m .updateMessageFunc (ctx , channelID , timestamp , text )
192217 }
193218 return nil
194219}
195220
196221func (m * mockSlackClient ) UpdateDMMessage (ctx context.Context , userID , prURL , text string ) error {
222+ m .mu .Lock ()
197223 m .updatedDMMessage = append (m .updatedDMMessage , mockUpdatedDMMessage {
198224 UserID : userID ,
199225 PRURL : prURL ,
200226 Text : text ,
201227 })
228+ m .mu .Unlock ()
202229 if m .updateDMMessageFunc != nil {
203230 return m .updateDMMessageFunc (ctx , userID , prURL , text )
204231 }
@@ -363,6 +390,7 @@ func (m *mockUserMapper) FormatUserMentions(ctx context.Context, githubUsers []s
363390
364391// mockTracker is a simple mock for notification tracking in tests.
365392type mockTracker struct {
393+ mu sync.Mutex
366394 channelNotified bool
367395 userTags []mockUserTag
368396 tagInfoByUser map [string ]TagInfo // Map from slackUserID to TagInfo for testing
@@ -378,10 +406,14 @@ type mockUserTag struct {
378406}
379407
380408func (m * mockTracker ) UpdateChannelNotification (workspaceID , owner , repo string , prNumber int ) {
409+ m .mu .Lock ()
410+ defer m .mu .Unlock ()
381411 m .channelNotified = true
382412}
383413
384414func (m * mockTracker ) UpdateUserPRChannelTag (workspaceID , slackUserID , channelID , owner , repo string , prNumber int ) {
415+ m .mu .Lock ()
416+ defer m .mu .Unlock ()
385417 m .userTags = append (m .userTags , mockUserTag {
386418 workspaceID : workspaceID ,
387419 slackUserID : slackUserID ,
@@ -393,6 +425,8 @@ func (m *mockTracker) UpdateUserPRChannelTag(workspaceID, slackUserID, channelID
393425}
394426
395427func (m * mockTracker ) LastUserPRChannelTag (workspaceID , slackUserID , owner , repo string , prNumber int ) TagInfo {
428+ m .mu .Lock ()
429+ defer m .mu .Unlock ()
396430 if m .tagInfoByUser != nil {
397431 if tagInfo , ok := m .tagInfoByUser [slackUserID ]; ok {
398432 return tagInfo
@@ -403,6 +437,7 @@ func (m *mockTracker) LastUserPRChannelTag(workspaceID, slackUserID, owner, repo
403437
404438// mockNotifier is a simple mock for notification manager in tests.
405439type mockNotifier struct {
440+ mu sync.Mutex
406441 Tracker * mockTracker
407442 notifyUserError error
408443 notifyCalls []notifyUserCall
@@ -417,12 +452,14 @@ type notifyUserCall struct {
417452
418453// NotifyUser mocks the notify.Manager.NotifyUser method.
419454func (m * mockNotifier ) NotifyUser (ctx context.Context , workspaceID , userID , channelID , channelName string , pr interface {}) error {
455+ m .mu .Lock ()
420456 m .notifyCalls = append (m .notifyCalls , notifyUserCall {
421457 workspaceID : workspaceID ,
422458 userID : userID ,
423459 channelID : channelID ,
424460 channelName : channelName ,
425461 })
462+ m .mu .Unlock ()
426463 return m .notifyUserError
427464}
428465
0 commit comments