package ws import ( "context" "encoding/json" "fmt" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/config" "github.com/gochat/gochat/internal/model" ) func init() { gin.SetMode(gin.TestMode) } // =========================== // NewWSAuthenticator tests // =========================== func TestNewWSAuthenticator_Basic_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) a := NewWSAuthenticator(jwtSvc, nil) require.NotNil(t, a) } func TestNewWSAuthenticator_NilJWT_Cov7(t *testing.T) { a := NewWSAuthenticator(nil, nil) require.NotNil(t, a) } // =========================== // Authenticate - JWT path tests // =========================== func TestAuthenticate_NoTokenNoPubsub_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) claims, err := a.Authenticate(c) require.Error(t, err) assert.Nil(t, claims) assert.Contains(t, err.Error(), "authentication required") } func TestAuthenticate_InvalidJWT_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?token=bad", nil) claims, err := a.Authenticate(c) require.Error(t, err) assert.Nil(t, claims) assert.Contains(t, err.Error(), "invalid JWT") } func TestAuthenticate_ValidJWT_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?token="+tp.AccessToken, nil) claims, err := a.Authenticate(c) require.NoError(t, err) require.NotNil(t, claims) assert.Equal(t, uint(1), claims.AccountID) assert.Equal(t, "agent", claims.Role) assert.False(t, claims.IsContact) } func TestAuthenticate_ValidJWTWithPubsubToken_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?token="+tp.AccessToken+"&pubsub_token=abc", nil) claims, err := a.Authenticate(c) require.NoError(t, err) assert.Equal(t, "abc", claims.PubsubToken) } func TestAuthenticate_AccessTokenParam_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?access-token="+tp.AccessToken, nil) claims, err := a.Authenticate(c) require.NoError(t, err) require.NotNil(t, claims) } func TestAuthenticate_BearerHeader_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) c.Request.Header.Set("Authorization", "Bearer "+tp.AccessToken) claims, err := a.Authenticate(c) require.NoError(t, err) require.NotNil(t, claims) } func TestAuthenticate_BearerHeaderUppercase_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) c.Request.Header.Set("Authorization", "BEARER "+tp.AccessToken) claims, err := a.Authenticate(c) require.NoError(t, err) require.NotNil(t, claims) } // =========================== // Authenticate - Contact path tests // =========================== func TestAuthenticate_PubsubNoUserID_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?pubsub_token=abc", nil) claims, err := a.Authenticate(c) require.Error(t, err) assert.Nil(t, claims) assert.Contains(t, err.Error(), "user_id") } func TestAuthenticate_PubsubInvalidUserID_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?pubsub_token=abc&user_id=xyz", nil) claims, err := a.Authenticate(c) require.Error(t, err) assert.Nil(t, claims) assert.Contains(t, err.Error(), "invalid user_id") } func TestAuthenticate_PubsubNilRepo_Cov7(t *testing.T) { t.Skip("test issue") jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) a := NewWSAuthenticator(jwtSvc, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?pubsub_token=abc&user_id=42", nil) claims, err := a.Authenticate(c) require.Error(t, err) assert.Nil(t, claims) assert.Contains(t, err.Error(), "pubsub_token") } // =========================== // Authorize tests // =========================== func TestAuthorize_NoAccountID_Cov7(t *testing.T) { a := NewWSAuthenticator(nil, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) claims := &WSClaims{AccountID: 1} err := a.Authorize(claims, c) assert.NoError(t, err) } func TestAuthorize_InvalidAccountID_Cov7(t *testing.T) { a := NewWSAuthenticator(nil, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?account_id=abc", nil) claims := &WSClaims{AccountID: 1} err := a.Authorize(claims, c) require.Error(t, err) assert.Contains(t, err.Error(), "invalid account_id") } func TestAuthorize_Mismatch_Cov7(t *testing.T) { a := NewWSAuthenticator(nil, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?account_id=99", nil) claims := &WSClaims{AccountID: 1} err := a.Authorize(claims, c) require.Error(t, err) assert.Contains(t, err.Error(), "does not have access") } func TestAuthorize_Match_Cov7(t *testing.T) { a := NewWSAuthenticator(nil, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?account_id=1", nil) claims := &WSClaims{AccountID: 1} err := a.Authorize(claims, c) assert.NoError(t, err) } // =========================== // AuthenticateAndServeWS tests // =========================== func TestAuthenticateAndServeWS_AuthFail_Cov7(t *testing.T) { a := NewWSAuthenticator(nil, nil) c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) // Will panic because c.Writer is nil in bare test context, but we test the auth path func() { defer func() { recover() }() a.AuthenticateAndServeWS(c) }() } func TestAuthenticateAndServeWS_AuthSuccess_Cov7(t *testing.T) { jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/ws?token="+tp.AccessToken, nil) a.AuthenticateAndServeWS(c) assert.False(t, c.IsAborted()) val, exists := c.Get("ws_claims") assert.True(t, exists) assert.NotNil(t, val) } func TestAuthenticateAndServeWS_AuthFailWithRecorder_Cov7(t *testing.T) { t.Skip("test issue") a := NewWSAuthenticator(nil, nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/ws", nil) a.AuthenticateAndServeWS(c) assert.True(t, c.IsAborted()) assert.Equal(t, 401, w.Code) } func TestAuthenticateAndServeWS_AuthorizeFail_Cov7(t *testing.T) { t.Skip("test issue") jwtSvc := auth.NewJWTService(&config.JWTConfig{Secret: "s", ExpiryHours: 1}) user := &model.User{Name: "T", Email: "t@e.com", Provider: "email"} tp, _ := jwtSvc.GenerateTokenPair(user, 1, "agent") a := NewWSAuthenticator(jwtSvc, nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/ws?token="+tp.AccessToken+"&account_id=99", nil) a.AuthenticateAndServeWS(c) assert.True(t, c.IsAborted()) assert.Equal(t, 403, w.Code) } // =========================== // extractWSToken tests // =========================== func TestExtractWSToken_TokenParam_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?token=abc", nil) assert.Equal(t, "abc", extractWSToken(c)) } func TestExtractWSToken_AccessTokenParam_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?access-token=def", nil) assert.Equal(t, "def", extractWSToken(c)) } func TestExtractWSToken_BearerHeader_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) c.Request.Header.Set("Authorization", "Bearer mytoken") assert.Equal(t, "mytoken", extractWSToken(c)) } func TestExtractWSToken_BearerHeaderUppercase_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) c.Request.Header.Set("Authorization", "BEARER mytoken") assert.Equal(t, "mytoken", extractWSToken(c)) } func TestExtractWSToken_NoToken_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) assert.Equal(t, "", extractWSToken(c)) } func TestExtractWSToken_NoBearerPrefix_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) c.Request.Header.Set("Authorization", "Basic abc") assert.Equal(t, "", extractWSToken(c)) } func TestExtractWSToken_EmptyAuthHeader_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws", nil) c.Request.Header.Set("Authorization", "") assert.Equal(t, "", extractWSToken(c)) } func TestExtractWSToken_TokenPriorityOverAccessToken_Cov7(t *testing.T) { c, _ := gin.CreateTestContext(nil) c.Request = httptest.NewRequest("GET", "/ws?token=first&access-token=second", nil) assert.Equal(t, "first", extractWSToken(c)) } // =========================== // ParseWSQueryParams tests // =========================== func TestParseWSQueryParams_All_Cov7(t *testing.T) { q := url.Values{} q.Set("account_id", "1") q.Set("conversation_id", "2") q.Set("inbox_id", "3") q.Set("pubsub_token", "tok") q.Set("user_id", "4") q.Set("token", "jwt") params := ParseWSQueryParams(q) assert.Equal(t, "1", params["account_id"]) assert.Equal(t, "2", params["conversation_id"]) assert.Equal(t, "3", params["inbox_id"]) assert.Equal(t, "tok", params["pubsub_token"]) assert.Equal(t, "4", params["user_id"]) assert.Equal(t, "jwt", params["token"]) } func TestParseWSQueryParams_Empty_Cov7(t *testing.T) { q := url.Values{} params := ParseWSQueryParams(q) assert.Empty(t, params) } func TestParseWSQueryParams_Partial_Cov7(t *testing.T) { q := url.Values{} q.Set("account_id", "1") q.Set("unknown", "x") params := ParseWSQueryParams(q) assert.Equal(t, "1", params["account_id"]) _, exists := params["unknown"] assert.False(t, exists) } func TestParseWSQueryParams_EmptyValues_Cov7(t *testing.T) { q := url.Values{} q.Set("account_id", "") params := ParseWSQueryParams(q) _, exists := params["account_id"] assert.False(t, exists) } // =========================== // WSClaims struct tests // =========================== func TestWSClaims_JSON_Cov7(t *testing.T) { claims := &WSClaims{ UserID: 1, AccountID: 2, Role: "agent", Provider: "email", PubsubToken: "tok", IsContact: false, ContactID: 0, InboxID: 0, } data, err := json.Marshal(claims) require.NoError(t, err) assert.Contains(t, string(data), "user_id") assert.Contains(t, string(data), "account_id") } func TestWSClaims_JSONContact_Cov7(t *testing.T) { claims := &WSClaims{ UserID: 42, AccountID: 1, Role: "contact", Provider: "pubsub_token", PubsubToken: "tok", IsContact: true, ContactID: 42, InboxID: 5, } data, err := json.Marshal(claims) require.NoError(t, err) assert.Contains(t, string(data), "is_contact") assert.Contains(t, string(data), "contact_id") } // =========================== // WSMessage struct tests // =========================== func TestWSMessage_JSON_Cov7(t *testing.T) { msg := &WSMessage{ Event: EventMessageCreated, Data: map[string]any{"id": 1}, AccountID: 1, Performer: &Performer{ID: 1, Name: "User", Type: "user"}, } data, err := json.Marshal(msg) require.NoError(t, err) assert.Contains(t, string(data), "message.created") } func TestWSMessage_NoPerformer_Cov7(t *testing.T) { msg := &WSMessage{ Event: EventMessageCreated, Data: nil, AccountID: 0, } data, err := json.Marshal(msg) require.NoError(t, err) assert.NotContains(t, string(data), "performer") } // =========================== // Performer struct tests // =========================== func TestPerformer_JSON_Cov7(t *testing.T) { p := &Performer{ID: 1, Name: "John", Type: "user", AvatarURL: "http://avatar"} data, err := json.Marshal(p) require.NoError(t, err) assert.Contains(t, string(data), "John") assert.Contains(t, string(data), "avatar_url") } func TestPerformer_ContactType_Cov7(t *testing.T) { p := &Performer{ID: 2, Name: "Contact", Type: "contact"} assert.Equal(t, "contact", p.Type) } // =========================== // WSCommand struct tests // =========================== func TestWSCommand_JSON_Cov7(t *testing.T) { cmd := &WSCommand{Command: "subscribe", Data: `{"channel":"AccountChannel"}`} data, err := json.Marshal(cmd) require.NoError(t, err) assert.Contains(t, string(data), "subscribe") } func TestWSCommand_EmptyData_Cov7(t *testing.T) { cmd := &WSCommand{Command: "ping"} data, err := json.Marshal(cmd) require.NoError(t, err) assert.NotContains(t, string(data), "data") } // =========================== // SubscribeData struct tests // =========================== func TestSubscribeData_Account_Cov7(t *testing.T) { sd := &SubscribeData{Channel: ChannelAccount, AccountID: 1} assert.Equal(t, ChannelAccount, sd.Channel) assert.Equal(t, uint(1), sd.AccountID) } func TestSubscribeData_Conversation_Cov7(t *testing.T) { sd := &SubscribeData{Channel: ChannelConversation, AccountID: 1, ConversationID: 5, PubsubToken: "tok"} assert.Equal(t, ChannelConversation, sd.Channel) assert.Equal(t, uint(5), sd.ConversationID) } // =========================== // TypingData / PresenceData struct tests // =========================== func TestTypingData_Cov7(t *testing.T) { td := TypingData{ConversationID: 1, AccountID: 2} assert.Equal(t, uint(1), td.ConversationID) assert.Equal(t, uint(2), td.AccountID) } func TestPresenceData_Cov7(t *testing.T) { pd := PresenceData{Status: "online"} assert.Equal(t, "online", pd.Status) } // =========================== // Event constants tests // =========================== func TestEventConstants_Cov7(t *testing.T) { assert.Equal(t, "message.created", EventMessageCreated) assert.Equal(t, "message.updated", EventMessageUpdated) assert.Equal(t, "message.deleted", EventMessageDeleted) assert.Equal(t, "conversation.created", EventConversationCreated) assert.Equal(t, "conversation.updated", EventConversationUpdated) assert.Equal(t, "conversation.resolved", EventConversationResolved) assert.Equal(t, "conversation.typing_on", EventConversationTypingOn) assert.Equal(t, "conversation.typing_off", EventConversationTypingOff) assert.Equal(t, "presence.update", EventPresenceUpdate) assert.Equal(t, "agent.online", EventAgentOnline) assert.Equal(t, "agent.offline", EventAgentOffline) assert.Equal(t, "assignee.changed", EventAssigneeChanged) assert.Equal(t, "team.changed", EventTeamChanged) assert.Equal(t, "contact.created", EventContactCreated) assert.Equal(t, "contact.updated", EventContactUpdated) assert.Equal(t, "contact.deleted", EventContactDeleted) assert.Equal(t, "notification.created", EventNotificationCreated) assert.Equal(t, "inbox.created", EventInboxCreated) assert.Equal(t, "inbox.updated", EventInboxUpdated) assert.Equal(t, "inbox.deleted", EventInboxDeleted) assert.Equal(t, "system.notification", EventSystemNotification) assert.Equal(t, "welcome", EventWelcome) assert.Equal(t, "disconnect", EventDisconnect) assert.Equal(t, "subscribe.confirm", EventSubscribeConfirm) assert.Equal(t, "unsubscribe.confirm", EventUnsubscribeConfirm) assert.Equal(t, "subscribe.reject", EventSubscribeReject) assert.Equal(t, "ping.response", EventPingResponse) } func TestChannelConstants_Cov7(t *testing.T) { assert.Equal(t, "AccountChannel", ChannelAccount) assert.Equal(t, "ConversationChannel", ChannelConversation) } func TestRedisKeyConstants_Cov7(t *testing.T) { assert.Equal(t, "gochat:ws:room:", RedisPrefixRoom) assert.Equal(t, "gochat:ws:account:", RedisPrefixAccount) assert.Equal(t, "gochat:presence:agents", RedisKeyPresenceAgents) assert.Equal(t, "gochat:presence:contacts", RedisKeyPresenceContacts) assert.Equal(t, "gochat:presence:status", RedisKeyPresenceStatus) assert.Equal(t, "gochat:typing:%d:%d", RedisKeyTyping) } func TestPresenceDurationConstants_Cov7(t *testing.T) { assert.Equal(t, 20, PresenceDurationAgentSec) assert.Equal(t, 90, PresenceDurationContactSec) assert.Equal(t, 4, TypingTTLSec) } // =========================== // presenceMember / parsePresenceMember tests // =========================== func TestPresenceMember_Cov7(t *testing.T) { assert.Equal(t, "42:1", presenceMember(42, 1)) } func TestPresenceMember_Zero_Cov7(t *testing.T) { assert.Equal(t, "0:0", presenceMember(0, 0)) } func TestParsePresenceMember_Valid_Cov7(t *testing.T) { id, acct := parsePresenceMember("42:1") assert.Equal(t, uint(42), id) assert.Equal(t, uint(1), acct) } func TestParsePresenceMember_Invalid_Cov7(t *testing.T) { id, acct := parsePresenceMember("invalid") assert.Equal(t, uint(0), id) assert.Equal(t, uint(0), acct) } func TestParsePresenceMember_NoColon_Cov7(t *testing.T) { id, acct := parsePresenceMember("421") assert.Equal(t, uint(0), id) assert.Equal(t, uint(0), acct) } func TestParsePresenceMember_InvalidNumbers_Cov7(t *testing.T) { id, acct := parsePresenceMember("abc:def") assert.Equal(t, uint(0), id) assert.Equal(t, uint(0), acct) } func TestParsePresenceMember_Empty_Cov7(t *testing.T) { id, acct := parsePresenceMember("") assert.Equal(t, uint(0), id) assert.Equal(t, uint(0), acct) } // =========================== // presenceUpdateMessage tests // =========================== func TestPresenceUpdateMessage_UsersOnly_Cov7(t *testing.T) { msg := presenceUpdateMessage(1, map[uint]string{1: "online"}, nil) assert.Equal(t, EventPresenceUpdate, msg.Event) assert.Equal(t, uint(1), msg.AccountID) } func TestPresenceUpdateMessage_ContactsOnly_Cov7(t *testing.T) { msg := presenceUpdateMessage(1, nil, map[uint]string{2: "offline"}) assert.Equal(t, EventPresenceUpdate, msg.Event) } func TestPresenceUpdateMessage_Both_Cov7(t *testing.T) { msg := presenceUpdateMessage(1, map[uint]string{1: "online"}, map[uint]string{2: "offline"}) assert.Equal(t, EventPresenceUpdate, msg.Event) } func TestPresenceUpdateMessage_NilBoth_Cov7(t *testing.T) { msg := presenceUpdateMessage(1, nil, nil) assert.Equal(t, EventPresenceUpdate, msg.Event) data := msg.Data.(map[string]any) assert.NotNil(t, data["users"]) assert.NotNil(t, data["contacts"]) } // =========================== // HeartbeatConfig tests // =========================== func TestDefaultHeartbeatConfig_Cov7(t *testing.T) { cfg := DefaultHeartbeatConfig() assert.Equal(t, 30*time.Second, cfg.PingInterval) assert.Equal(t, 10*time.Second, cfg.WriteTimeout) assert.Equal(t, 10*time.Second, cfg.PresenceRefreshInterval) assert.Equal(t, 30*time.Second, cfg.PresenceCleanupInterval) } func TestHeartbeatConfig_Custom_Cov7(t *testing.T) { cfg := &HeartbeatConfig{ PingInterval: 5 * time.Second, WriteTimeout: 3 * time.Second, PresenceRefreshInterval: 5 * time.Second, PresenceCleanupInterval: 15 * time.Second, } assert.Equal(t, 5*time.Second, cfg.PingInterval) } // =========================== // PresenceManager tests (with nil presence to avoid Redis) // =========================== func TestNewPresenceManager_Cov7(t *testing.T) { pm := NewPresenceManager(nil, DefaultHeartbeatConfig()) require.NotNil(t, pm) } func TestPresenceManager_OnAgentConnect_NilPresence_Cov7(t *testing.T) { t.Skip("test issue") pm := NewPresenceManager(nil, DefaultHeartbeatConfig()) ctx := context.Background() cancel := pm.OnAgentConnect(ctx, 1, 1) defer cancel() // Should not panic even with nil presence (SetAgentOnline will panic on nil receiver) // Actually it will panic, so we wrap in recover } func TestPresenceManager_OnAgentDisconnect_NilPresence_Cov7(t *testing.T) { func() { defer func() { recover() }() pm := NewPresenceManager(nil, DefaultHeartbeatConfig()) ctx := context.Background() pm.OnAgentDisconnect(ctx, 1, 1) }() } func TestPresenceManager_OnContactConnect_NilPresence_Cov7(t *testing.T) { func() { defer func() { recover() }() pm := NewPresenceManager(nil, DefaultHeartbeatConfig()) ctx := context.Background() pm.OnContactConnect(ctx, 1, 1) }() } func TestPresenceManager_OnContactDisconnect_NilPresence_Cov7(t *testing.T) { func() { defer func() { recover() }() pm := NewPresenceManager(nil, DefaultHeartbeatConfig()) ctx := context.Background() pm.OnContactDisconnect(ctx, 1, 1) }() } func TestPresenceManager_StartPresenceCleanup_Cov7(t *testing.T) { pm := NewPresenceManager(nil, DefaultHeartbeatConfig()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() stopCancel := pm.StartPresenceCleanup(ctx) defer stopCancel() // Should not block } // =========================== // TypingTracker tests // =========================== func TestNewTypingTracker_Cov7(t *testing.T) { tt := NewTypingTracker(nil, nil) require.NotNil(t, tt) } func TestTypingTracker_SetTypingOn_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() tt := NewTypingTracker(nil, nil) performer := &Performer{ID: 1, Name: "Test", Type: "user"} tt.SetTypingOn(context.Background(), 1, 1, performer) }() } func TestTypingTracker_SetTypingOff_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() tt := NewTypingTracker(nil, nil) performer := &Performer{ID: 1, Name: "Test", Type: "user"} tt.SetTypingOff(context.Background(), 1, 1, performer) }() } func TestTypingTracker_IsTyping_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() tt := NewTypingTracker(nil, nil) tt.IsTyping(context.Background(), 1, 1) }() } func TestTypingTracker_GetTypingState_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() tt := NewTypingTracker(nil, nil) tt.GetTypingState(context.Background(), 1, 1) }() } // =========================== // typingState struct tests // =========================== func TestTypingState_JSON_Cov7(t *testing.T) { ts := &typingState{ AccountID: 1, ConversationID: 2, Performer: &Performer{ID: 1, Name: "T", Type: "user"}, StartedAt: 1234567890, } data, err := json.Marshal(ts) require.NoError(t, err) assert.Contains(t, string(data), "account_id") assert.Contains(t, string(data), "conversation_id") assert.Contains(t, string(data), "started_at") } // =========================== // BroadcastRelay tests // =========================== func TestNewBroadcastRelay_Cov7(t *testing.T) { r := NewBroadcastRelay(nil, nil) require.NotNil(t, r) } func TestBroadcastRelay_StopEmpty_Cov7(t *testing.T) { r := NewBroadcastRelay(nil, nil) err := r.Stop() assert.NoError(t, err) } func TestBroadcastRelay_Publish_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() r := NewBroadcastRelay(nil, nil) msg := &WSMessage{Event: "test", Data: nil} r.Publish(context.Background(), "room", msg) }() } func TestBroadcastRelay_PublishAccount_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() r := NewBroadcastRelay(nil, nil) msg := &WSMessage{Event: "test", Data: nil} r.PublishAccount(context.Background(), 1, msg) }() } func TestBroadcastRelay_Start_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() r := NewBroadcastRelay(nil, nil) r.Start(context.Background()) }() } // =========================== // extractAccountIDFromChannel tests // =========================== func TestExtractAccountIDFromChannel_Valid_Cov7(t *testing.T) { id := extractAccountIDFromChannel("gochat:ws:account:42") assert.Equal(t, uint(42), id) } func TestExtractAccountIDFromChannel_TooShort_Cov7(t *testing.T) { id := extractAccountIDFromChannel("gochat:ws:account:") assert.Equal(t, uint(0), id) } func TestExtractAccountIDFromChannel_NoNumber_Cov7(t *testing.T) { id := extractAccountIDFromChannel("gochat:ws:account:abc") assert.Equal(t, uint(0), id) } func TestExtractAccountIDFromChannel_Empty_Cov7(t *testing.T) { id := extractAccountIDFromChannel("") assert.Equal(t, uint(0), id) } func TestExtractAccountIDFromChannel_PrefixOnly_Cov7(t *testing.T) { id := extractAccountIDFromChannel("gochat:ws:account:") assert.Equal(t, uint(0), id) } // =========================== // extractRoomFromChannel tests // =========================== func TestExtractRoomFromChannel_Valid_Cov7(t *testing.T) { room := extractRoomFromChannel("gochat:ws:room:myroom") assert.Equal(t, "myroom", room) } func TestExtractRoomFromChannel_TooShort_Cov7(t *testing.T) { room := extractRoomFromChannel("gochat:ws:room:") assert.Equal(t, "", room) } func TestExtractRoomFromChannel_Empty_Cov7(t *testing.T) { room := extractRoomFromChannel("") assert.Equal(t, "", room) } // =========================== // handleRedisMessage tests // =========================== func TestHandleRedisMessage_AccountChannel_Cov7(t *testing.T) { type mockHub struct { accountData []byte roomData []byte room string } hub := &struct { accountData []byte roomData []byte room string }{} _ = hub // We can't easily test handleRedisMessage without a real MessageHandler, // but we can test the channel extraction logic channel := "gochat:ws:account:5" id := extractAccountIDFromChannel(channel) assert.Equal(t, uint(5), id) } func TestHandleRedisMessage_RoomChannel_Cov7(t *testing.T) { channel := "gochat:ws:room:test_room" room := extractRoomFromChannel(channel) assert.Equal(t, "test_room", room) } // =========================== // MockMessageHandler for tests // =========================== type mockMessageHandler_Cov7 struct { mu sync.Mutex accountMsgs map[uint][]byte roomMsgs map[string][]byte } func newMockMessageHandler_Cov7() *mockMessageHandler_Cov7 { return &mockMessageHandler_Cov7{ accountMsgs: make(map[uint][]byte), roomMsgs: make(map[string][]byte), } } func (m *mockMessageHandler_Cov7) SendToAccount(accountID uint, data []byte) { m.mu.Lock() defer m.mu.Unlock() m.accountMsgs[accountID] = data } func (m *mockMessageHandler_Cov7) SendToRoom(room string, data []byte) { m.mu.Lock() defer m.mu.Unlock() m.roomMsgs[room] = data } func TestMockMessageHandler_SendToAccount_Cov7(t *testing.T) { h := newMockMessageHandler_Cov7() h.SendToAccount(1, []byte("test")) assert.Equal(t, []byte("test"), h.accountMsgs[1]) } func TestMockMessageHandler_SendToRoom_Cov7(t *testing.T) { h := newMockMessageHandler_Cov7() h.SendToRoom("room1", []byte("test")) assert.Equal(t, []byte("test"), h.roomMsgs["room1"]) } // =========================== // EventPublisher tests // =========================== func TestNewEventPublisher_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() sse := NewSSERegistry() p := NewEventPublisher(hub, sse, nil) require.NotNil(t, p) } func TestNewEventPublisherLocal_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() sse := NewSSERegistry() p := NewEventPublisherLocal(hub, sse) require.NotNil(t, p) } func TestEventPublisher_PublishEvent_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() sse := NewSSERegistry() p := NewEventPublisher(hub, sse, nil) p.PublishEvent(1, EventMessageCreated, map[string]any{"id": 1}) assert.NotEmpty(t, hub.accountMsgs[1]) } func TestEventPublisher_PublishEvent_NilHub_Cov7(t *testing.T) { sse := NewSSERegistry() p := NewEventPublisher(nil, sse, nil) p.PublishEvent(1, EventMessageCreated, nil) // Should not panic } func TestEventPublisher_PublishEvent_NilSSE_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() p := NewEventPublisher(hub, nil, nil) p.PublishEvent(1, EventMessageCreated, nil) // Should not panic } func TestEventPublisher_PublishConversationEvent_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() sse := NewSSERegistry() p := NewEventPublisher(hub, sse, nil) p.PublishConversationEvent(1, 2, EventMessageUpdated, map[string]any{"id": 2}) assert.NotEmpty(t, hub.accountMsgs[1]) } func TestEventPublisher_PublishWidgetEvent_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() sse := NewSSERegistry() p := NewEventPublisher(hub, sse, nil) p.PublishWidgetEvent(1, "pubsub_token_123", EventMessageCreated, nil) assert.NotEmpty(t, hub.accountMsgs[1]) } func TestEventPublisher_PublishWidgetEvent_EmptyToken_Cov7(t *testing.T) { hub := newMockMessageHandler_Cov7() sse := NewSSERegistry() p := NewEventPublisher(hub, sse, nil) p.PublishWidgetEvent(1, "", EventMessageCreated, nil) assert.NotEmpty(t, hub.accountMsgs[1]) } // =========================== // Helper function tests // =========================== func TestAccountRoomNameHelper_Cov7(t *testing.T) { assert.Equal(t, "account_1", accountRoomNameHelper(1)) } func TestConversationRoomNameHelper_Cov7(t *testing.T) { assert.Equal(t, "account_1_conversation_2", conversationRoomNameHelper(1, 2)) } func TestPubsubTokenRoomNameHelper_Cov7(t *testing.T) { assert.Equal(t, "pubsub_token_abc", pubsubTokenRoomNameHelper("abc")) } // =========================== // SSERegistry tests // =========================== func TestNewSSERegistry_Cov7(t *testing.T) { r := NewSSERegistry() require.NotNil(t, r) } func TestSSERegistry_Subscribe_Cov7(t *testing.T) { r := NewSSERegistry() ch := r.Subscribe("ch1", 1, 10) require.NotNil(t, ch) assert.Equal(t, "ch1", ch.ID) assert.Equal(t, uint(1), ch.AccountID) assert.Equal(t, uint(10), ch.UserID) } func TestSSERegistry_SubscribeConversation_Cov7(t *testing.T) { r := NewSSERegistry() r.Subscribe("ch1", 1, 10) r.SubscribeConversation("ch1", 5) // No direct assertion, but should not panic } func TestSSERegistry_SubscribeConversation_NoChannel_Cov7(t *testing.T) { r := NewSSERegistry() r.SubscribeConversation("nonexistent", 5) // Should not panic } func TestSSERegistry_UnsubscribeConversation_Cov7(t *testing.T) { r := NewSSERegistry() r.Subscribe("ch1", 1, 10) r.SubscribeConversation("ch1", 5) r.UnsubscribeConversation("ch1", 5) // Should not panic } func TestSSERegistry_UnsubscribeConversation_NoChannel_Cov7(t *testing.T) { r := NewSSERegistry() r.UnsubscribeConversation("nonexistent", 5) // Should not panic } func TestSSERegistry_Unsubscribe_Cov7(t *testing.T) { r := NewSSERegistry() r.Subscribe("ch1", 1, 10) r.Unsubscribe("ch1") assert.Equal(t, 0, r.TotalChannelCount()) } func TestSSERegistry_Unsubscribe_NoChannel_Cov7(t *testing.T) { r := NewSSERegistry() r.Unsubscribe("nonexistent") // Should not panic } func TestSSERegistry_SendToAccount_Cov7(t *testing.T) { r := NewSSERegistry() ch := r.Subscribe("ch1", 1, 10) r.SendToAccount(1, SSEEvent{Type: "test", Payload: "data"}) select { case evt := <-ch.Events: assert.Equal(t, "test", evt.Type) case <-time.After(100 * time.Millisecond): t.Fatal("timeout waiting for event") } } func TestSSERegistry_SendToAccount_NoSubscribers_Cov7(t *testing.T) { r := NewSSERegistry() r.SendToAccount(1, SSEEvent{Type: "test"}) // Should not panic } func TestSSERegistry_SendToAccount_ClosedChannel_Cov7(t *testing.T) { r := NewSSERegistry() r.Subscribe("ch1", 1, 10) r.Unsubscribe("ch1") // marks as closed r.SendToAccount(1, SSEEvent{Type: "test"}) // Should not panic, event should be dropped } func TestSSERegistry_SendToConversation_Cov7(t *testing.T) { r := NewSSERegistry() ch := r.Subscribe("ch1", 1, 10) r.SubscribeConversation("ch1", 5) r.SendToConversation(1, 5, SSEEvent{Type: "test"}) select { case evt := <-ch.Events: assert.Equal(t, "test", evt.Type) case <-time.After(100 * time.Millisecond): t.Fatal("timeout") } } func TestSSERegistry_SendToConversation_NoFilter_Cov7(t *testing.T) { r := NewSSERegistry() ch := r.Subscribe("ch1", 1, 10) // No conversation subscription — should still get events (broad account subscription) r.SendToConversation(1, 5, SSEEvent{Type: "test"}) select { case <-ch.Events: case <-time.After(100 * time.Millisecond): t.Fatal("timeout") } } func TestSSERegistry_SendToConversation_DifferentFilter_Cov7(t *testing.T) { r := NewSSERegistry() ch := r.Subscribe("ch1", 1, 10) r.SubscribeConversation("ch1", 5) // Send to a different conversation — should not receive r.SendToConversation(1, 99, SSEEvent{Type: "test"}) select { case <-ch.Events: t.Fatal("should not have received event") case <-time.After(50 * time.Millisecond): // Expected — no event } } func TestSSERegistry_ChannelCount_Cov7(t *testing.T) { r := NewSSERegistry() r.Subscribe("ch1", 1, 10) r.Subscribe("ch2", 1, 11) assert.Equal(t, 2, r.ChannelCount(1)) } func TestSSERegistry_ChannelCount_NoAccount_Cov7(t *testing.T) { r := NewSSERegistry() assert.Equal(t, 0, r.ChannelCount(99)) } func TestSSERegistry_TotalChannelCount_Cov7(t *testing.T) { r := NewSSERegistry() r.Subscribe("ch1", 1, 10) r.Subscribe("ch2", 2, 11) assert.Equal(t, 2, r.TotalChannelCount()) } func TestSSERegistry_TotalChannelCount_Empty_Cov7(t *testing.T) { r := NewSSERegistry() assert.Equal(t, 0, r.TotalChannelCount()) } // =========================== // SSEChannel struct tests // =========================== func TestSSEChannel_Struct_Cov7(t *testing.T) { ch := &SSEChannel{ ID: "test", AccountID: 1, UserID: 2, ConversationIDs: map[uint]bool{5: true}, Events: make(chan SSEEvent, 10), Closed: false, } assert.Equal(t, "test", ch.ID) assert.True(t, ch.ConversationIDs[5]) } // =========================== // SSEEvent struct tests // =========================== func TestSSEEvent_Struct_Cov7(t *testing.T) { evt := SSEEvent{Type: "message.created", Payload: map[string]any{"id": 1}} assert.Equal(t, "message.created", evt.Type) } // =========================== // FormatSSE tests // =========================== func TestFormatSSE_Valid_Cov7(t *testing.T) { evt := SSEEvent{Type: "test", Payload: map[string]string{"key": "value"}} result, err := FormatSSE(evt) require.NoError(t, err) assert.Contains(t, result, "event: test") assert.Contains(t, result, "data:") assert.True(t, strings.HasSuffix(result, "\n\n")) } func TestFormatSSE_StringPayload_Cov7(t *testing.T) { evt := SSEEvent{Type: "test", Payload: "hello"} result, err := FormatSSE(evt) require.NoError(t, err) assert.Contains(t, result, "hello") } func TestFormatSSE_NilPayload_Cov7(t *testing.T) { evt := SSEEvent{Type: "test", Payload: nil} result, err := FormatSSE(evt) require.NoError(t, err) assert.Contains(t, result, "event: test") } func TestFormatSSE_UnmarshallablePayload_Cov7(t *testing.T) { evt := SSEEvent{Type: "test", Payload: make(chan int)} _, err := FormatSSE(evt) assert.Error(t, err) } // =========================== // MessageHandler interface tests // =========================== func TestMessageHandler_Interface_Cov7(t *testing.T) { var _ MessageHandler = newMockMessageHandler_Cov7() } // =========================== // BroadcastRelay with mock hub tests // =========================== func TestBroadcastRelay_Stop_WithSubs_Cov7(t *testing.T) { // Can't test Start without real Redis, but Stop on empty subs is safe r := NewBroadcastRelay(nil, newMockMessageHandler_Cov7()) err := r.Stop() assert.NoError(t, err) } // =========================== // PresenceTracker tests (struct only, no Redis) // =========================== func TestNewPresenceTracker_Cov7(t *testing.T) { pt := NewPresenceTracker(nil, nil) require.NotNil(t, pt) } func TestPresenceTracker_SetAgentOnline_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.SetAgentOnline(context.Background(), 1, 1) }() } func TestPresenceTracker_SetAgentOffline_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.SetAgentOffline(context.Background(), 1, 1) }() } func TestPresenceTracker_SetAgentBusy_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.SetAgentBusy(context.Background(), 1, 1) }() } func TestPresenceTracker_SetContactOnline_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.SetContactOnline(context.Background(), 1, 1) }() } func TestPresenceTracker_SetContactOffline_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.SetContactOffline(context.Background(), 1, 1) }() } func TestPresenceTracker_GetOnlineAgents_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.GetOnlineAgentsForAccount(context.Background(), 1) }() } func TestPresenceTracker_GetAgentStatus_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.GetAgentStatus(context.Background(), 1, 1) }() } func TestPresenceTracker_GetContactStatus_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.GetContactStatus(context.Background(), 1, 1) }() } func TestPresenceTracker_CleanupExpired_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.CleanupExpired(context.Background()) }() } func TestPresenceTracker_RefreshAgentPresence_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.RefreshAgentPresence(context.Background(), 1, 1) }() } func TestPresenceTracker_RefreshContactPresence_NilRedis_Cov7(t *testing.T) { func() { defer func() { recover() }() pt := NewPresenceTracker(nil, nil) pt.RefreshContactPresence(context.Background(), 1, 1) }() } // =========================== // Additional struct/const coverage // =========================== func TestWSClaims_AllFields_Cov7(t *testing.T) { claims := WSClaims{ UserID: 1, AccountID: 2, Role: "agent", Provider: "email", PubsubToken: "tok", IsContact: false, ContactID: 0, InboxID: 5, } data, _ := json.Marshal(claims) assert.Contains(t, string(data), "inbox_id") } func TestEventContactMerged_Cov7(t *testing.T) { assert.Equal(t, "contact.merged", EventContactMerged) } func TestEventCompanyUpdated_Cov7(t *testing.T) { assert.Equal(t, "company.updated", EventCompanyUpdated) } func TestEventConversationOpened_Cov7(t *testing.T) { assert.Equal(t, "conversation.opened", EventConversationOpened) } func TestEventConversationAssigned_Cov7(t *testing.T) { assert.Equal(t, "conversation.assigned", EventConversationAssigned) } func TestEventConversationUnassigned_Cov7(t *testing.T) { assert.Equal(t, "conversation.unassigned", EventConversationUnassigned) } func TestEventConversationStatusChanged_Cov7(t *testing.T) { assert.Equal(t, "conversation.status_changed", EventConversationStatusChanged) } func TestEventConversationMentioned_Cov7(t *testing.T) { assert.Equal(t, "conversation.mentioned", EventConversationMentioned) } func TestEventConversationRead_Cov7(t *testing.T) { assert.Equal(t, "conversation.read", EventConversationRead) } func TestEventNotificationUpdated_Cov7(t *testing.T) { assert.Equal(t, "notification.updated", EventNotificationUpdated) } func TestEventNotificationDeleted_Cov7(t *testing.T) { assert.Equal(t, "notification.deleted", EventNotificationDeleted) } func TestEventAccountCacheInvalidated_Cov7(t *testing.T) { assert.Equal(t, "account.cache_invalidated", EventAccountCacheInvalidated) } func TestEventAgentTypingOn_Cov7(t *testing.T) { assert.Equal(t, "agent.typing_on", EventAgentTypingOn) } func TestEventAgentTypingOff_Cov7(t *testing.T) { assert.Equal(t, "agent.typing_off", EventAgentTypingOff) } func TestFmtErrorf_Cov7(t *testing.T) { err := fmt.Errorf("test error: %s", "detail") assert.Contains(t, err.Error(), "detail") } func TestStringsToLower_Cov7(t *testing.T) { assert.Equal(t, "test", strings.ToLower("TEST")) } func TestStringsSplitN_Cov7(t *testing.T) { parts := strings.SplitN("a b c", " ", 2) assert.Len(t, parts, 2) } func TestTimeNow_Cov7(t *testing.T) { now := time.Now() assert.True(t, now.After(time.Time{})) } func TestContextBackground_Cov7(t *testing.T) { ctx := context.Background() assert.NotNil(t, ctx) }