2026-04-05 19:03:22 +02:00
|
|
|
package ws
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// allClientToServerTypes returns every client-to-server message type constant
|
|
|
|
|
// that should have a CommandConstructor entry (excludes "auth" which is handled
|
|
|
|
|
// separately by the auth flow).
|
|
|
|
|
func allClientToServerTypes() []string {
|
|
|
|
|
return []string{
|
|
|
|
|
MsgTypePing,
|
|
|
|
|
MsgTypeChatSend,
|
|
|
|
|
MsgTypeChatEdit,
|
|
|
|
|
MsgTypeChatDelete,
|
|
|
|
|
MsgTypeTypingStart,
|
|
|
|
|
MsgTypePresenceUpdate,
|
|
|
|
|
MsgTypeChannelFocus,
|
2026-08-01 22:06:14 +02:00
|
|
|
MsgTypeMarkRead,
|
2026-04-05 19:03:22 +02:00
|
|
|
MsgTypeReactionAdd,
|
|
|
|
|
MsgTypeReactionRemove,
|
|
|
|
|
MsgTypeVoiceJoin,
|
|
|
|
|
MsgTypeVoiceLeave,
|
|
|
|
|
MsgTypeVoiceTokenRefresh,
|
|
|
|
|
MsgTypeVoiceMute,
|
|
|
|
|
MsgTypeVoiceDeafen,
|
|
|
|
|
MsgTypeVoiceCamera,
|
|
|
|
|
MsgTypeVoiceScreenshare,
|
|
|
|
|
MsgTypeVoiceE2EEAnnounce,
|
|
|
|
|
MsgTypeVoiceE2EEOffer,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandConstructorsCoverage(t *testing.T) {
|
|
|
|
|
for _, msgType := range allClientToServerTypes() {
|
|
|
|
|
if _, ok := commandConstructors[msgType]; !ok {
|
|
|
|
|
t.Errorf("commandConstructors missing entry for %q", msgType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandTypeAndUserID(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
cmd Command
|
|
|
|
|
wantType string
|
|
|
|
|
wantUID int64
|
|
|
|
|
}{
|
|
|
|
|
{"PingCmd", PingCmd{userID: 1}, MsgTypePing, 1},
|
|
|
|
|
{"ChatSendCmd", ChatSendCmd{userID: 2, channelID: 10}, MsgTypeChatSend, 2},
|
|
|
|
|
{"ChatEditCmd", ChatEditCmd{userID: 3, messageID: 20}, MsgTypeChatEdit, 3},
|
|
|
|
|
{"ChatDeleteCmd", ChatDeleteCmd{userID: 4, messageID: 30}, MsgTypeChatDelete, 4},
|
|
|
|
|
{"TypingStartCmd", TypingStartCmd{userID: 5, channelID: 11}, MsgTypeTypingStart, 5},
|
|
|
|
|
{"PresenceUpdateCmd", PresenceUpdateCmd{userID: 6, status: "online"}, MsgTypePresenceUpdate, 6},
|
|
|
|
|
{"ChannelFocusCmd", ChannelFocusCmd{userID: 7, channelID: 12}, MsgTypeChannelFocus, 7},
|
2026-08-01 22:06:14 +02:00
|
|
|
{"MarkReadCmd", MarkReadCmd{userID: 7, channelID: 12}, MsgTypeMarkRead, 7},
|
2026-04-05 19:03:22 +02:00
|
|
|
{"ReactionAddCmd", ReactionAddCmd{userID: 8, messageID: 40, emoji: "👍"}, MsgTypeReactionAdd, 8},
|
|
|
|
|
{"ReactionRemoveCmd", ReactionRemoveCmd{userID: 9, messageID: 41, emoji: "👎"}, MsgTypeReactionRemove, 9},
|
|
|
|
|
{"VoiceJoinCmd", VoiceJoinCmd{userID: 10, channelID: 13}, MsgTypeVoiceJoin, 10},
|
|
|
|
|
{"VoiceLeaveCmd", VoiceLeaveCmd{userID: 11}, MsgTypeVoiceLeave, 11},
|
|
|
|
|
{"VoiceTokenRefreshCmd", VoiceTokenRefreshCmd{userID: 12}, MsgTypeVoiceTokenRefresh, 12},
|
|
|
|
|
{"VoiceMuteCmd", VoiceMuteCmd{userID: 13, muted: true}, MsgTypeVoiceMute, 13},
|
|
|
|
|
{"VoiceDeafenCmd", VoiceDeafenCmd{userID: 14, deafened: true}, MsgTypeVoiceDeafen, 14},
|
|
|
|
|
{"VoiceCameraCmd", VoiceCameraCmd{userID: 15, enabled: true}, MsgTypeVoiceCamera, 15},
|
|
|
|
|
{"VoiceScreenshareCmd", VoiceScreenshareCmd{userID: 16, enabled: true}, MsgTypeVoiceScreenshare, 16},
|
|
|
|
|
{"VoiceE2EEAnnounceCmd", VoiceE2EEAnnounceCmd{userID: 17, publicKey: "abc"}, MsgTypeVoiceE2EEAnnounce, 17},
|
|
|
|
|
{"VoiceE2EEOfferCmd", VoiceE2EEOfferCmd{userID: 18, targetUserID: 99}, MsgTypeVoiceE2EEOffer, 18},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
if got := tt.cmd.Type(); got != tt.wantType {
|
|
|
|
|
t.Errorf("Type() = %q, want %q", got, tt.wantType)
|
|
|
|
|
}
|
|
|
|
|
if got := tt.cmd.UserID(); got != tt.wantUID {
|
|
|
|
|
t.Errorf("UserID() = %d, want %d", got, tt.wantUID)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandChannelScoped(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
cmd Command
|
|
|
|
|
wantChID int64
|
|
|
|
|
isScoped bool
|
|
|
|
|
}{
|
|
|
|
|
{"ChatSendCmd", ChatSendCmd{channelID: 100}, 100, true},
|
|
|
|
|
{"TypingStartCmd", TypingStartCmd{channelID: 200}, 200, true},
|
|
|
|
|
{"ChannelFocusCmd", ChannelFocusCmd{channelID: 300}, 300, true},
|
2026-08-01 22:06:14 +02:00
|
|
|
{"MarkReadCmd", MarkReadCmd{channelID: 301}, 301, true},
|
2026-04-05 19:03:22 +02:00
|
|
|
{"VoiceJoinCmd", VoiceJoinCmd{channelID: 400}, 400, true},
|
|
|
|
|
{"PingCmd", PingCmd{userID: 1}, 0, false},
|
|
|
|
|
{"VoiceLeaveCmd", VoiceLeaveCmd{userID: 1}, 0, false},
|
|
|
|
|
{"PresenceUpdateCmd", PresenceUpdateCmd{userID: 1}, 0, false},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
cs, ok := tt.cmd.(ChannelScoped)
|
|
|
|
|
if ok != tt.isScoped {
|
|
|
|
|
t.Errorf("ChannelScoped assertion = %v, want %v", ok, tt.isScoped)
|
|
|
|
|
}
|
|
|
|
|
if ok && cs.ChannelID() != tt.wantChID {
|
|
|
|
|
t.Errorf("ChannelID() = %d, want %d", cs.ChannelID(), tt.wantChID)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandConstructorParseValid(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
msgType string
|
|
|
|
|
payload string
|
|
|
|
|
checkFn func(t *testing.T, cmd Command)
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "ping",
|
|
|
|
|
msgType: MsgTypePing,
|
|
|
|
|
payload: `{}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
if cmd.Type() != MsgTypePing {
|
|
|
|
|
t.Errorf("Type() = %q, want %q", cmd.Type(), MsgTypePing)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chat_send",
|
|
|
|
|
msgType: MsgTypeChatSend,
|
|
|
|
|
payload: `{"channel_id": 42, "content": "hello", "reply_to": 10, "attachments": ["a1"]}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
cs := cmd.(ChatSendCmd)
|
|
|
|
|
if cs.ChannelID() != 42 {
|
|
|
|
|
t.Errorf("ChannelID() = %d, want 42", cs.ChannelID())
|
|
|
|
|
}
|
|
|
|
|
if cs.Content() != "hello" {
|
|
|
|
|
t.Errorf("Content() = %q, want %q", cs.Content(), "hello")
|
|
|
|
|
}
|
|
|
|
|
if cs.ReplyTo() == nil || *cs.ReplyTo() != 10 {
|
|
|
|
|
t.Errorf("ReplyTo() = %v, want 10", cs.ReplyTo())
|
|
|
|
|
}
|
|
|
|
|
if len(cs.Attachments()) != 1 || cs.Attachments()[0] != "a1" {
|
|
|
|
|
t.Errorf("Attachments() = %v, want [a1]", cs.Attachments())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chat_edit",
|
|
|
|
|
msgType: MsgTypeChatEdit,
|
|
|
|
|
payload: `{"message_id": 99, "content": "updated"}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
ce := cmd.(ChatEditCmd)
|
|
|
|
|
if ce.MessageID() != 99 {
|
|
|
|
|
t.Errorf("MessageID() = %d, want 99", ce.MessageID())
|
|
|
|
|
}
|
|
|
|
|
if ce.Content() != "updated" {
|
|
|
|
|
t.Errorf("Content() = %q, want %q", ce.Content(), "updated")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chat_delete",
|
|
|
|
|
msgType: MsgTypeChatDelete,
|
|
|
|
|
payload: `{"message_id": 55}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
cd := cmd.(ChatDeleteCmd)
|
|
|
|
|
if cd.MessageID() != 55 {
|
|
|
|
|
t.Errorf("MessageID() = %d, want 55", cd.MessageID())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "typing_start",
|
|
|
|
|
msgType: MsgTypeTypingStart,
|
|
|
|
|
payload: `{"channel_id": 7}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
ts := cmd.(TypingStartCmd)
|
|
|
|
|
if ts.ChannelID() != 7 {
|
|
|
|
|
t.Errorf("ChannelID() = %d, want 7", ts.ChannelID())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "presence_update",
|
|
|
|
|
msgType: MsgTypePresenceUpdate,
|
|
|
|
|
payload: `{"status": "idle"}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
pu := cmd.(PresenceUpdateCmd)
|
|
|
|
|
if pu.Status() != "idle" {
|
|
|
|
|
t.Errorf("Status() = %q, want %q", pu.Status(), "idle")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "channel_focus",
|
|
|
|
|
msgType: MsgTypeChannelFocus,
|
|
|
|
|
payload: `{"channel_id": 33}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
cf := cmd.(ChannelFocusCmd)
|
|
|
|
|
if cf.ChannelID() != 33 {
|
|
|
|
|
t.Errorf("ChannelID() = %d, want 33", cf.ChannelID())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-08-01 22:06:14 +02:00
|
|
|
{
|
|
|
|
|
name: "mark_read",
|
|
|
|
|
msgType: MsgTypeMarkRead,
|
|
|
|
|
payload: `{"channel_id": 34}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
mr := cmd.(MarkReadCmd)
|
|
|
|
|
if mr.ChannelID() != 34 {
|
|
|
|
|
t.Errorf("ChannelID() = %d, want 34", mr.ChannelID())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-04-05 19:03:22 +02:00
|
|
|
{
|
|
|
|
|
name: "reaction_add",
|
|
|
|
|
msgType: MsgTypeReactionAdd,
|
|
|
|
|
payload: `{"message_id": 77, "emoji": "🔥"}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
ra := cmd.(ReactionAddCmd)
|
|
|
|
|
if ra.MessageID() != 77 {
|
|
|
|
|
t.Errorf("MessageID() = %d, want 77", ra.MessageID())
|
|
|
|
|
}
|
|
|
|
|
if ra.Emoji() != "🔥" {
|
|
|
|
|
t.Errorf("Emoji() = %q, want %q", ra.Emoji(), "🔥")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "reaction_remove",
|
|
|
|
|
msgType: MsgTypeReactionRemove,
|
|
|
|
|
payload: `{"message_id": 88, "emoji": "👎"}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
rr := cmd.(ReactionRemoveCmd)
|
|
|
|
|
if rr.MessageID() != 88 {
|
|
|
|
|
t.Errorf("MessageID() = %d, want 88", rr.MessageID())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_join",
|
|
|
|
|
msgType: MsgTypeVoiceJoin,
|
|
|
|
|
payload: `{"channel_id": 50}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
vj := cmd.(VoiceJoinCmd)
|
|
|
|
|
if vj.ChannelID() != 50 {
|
|
|
|
|
t.Errorf("ChannelID() = %d, want 50", vj.ChannelID())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_leave",
|
|
|
|
|
msgType: MsgTypeVoiceLeave,
|
|
|
|
|
payload: `{}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
if cmd.Type() != MsgTypeVoiceLeave {
|
|
|
|
|
t.Errorf("Type() = %q, want %q", cmd.Type(), MsgTypeVoiceLeave)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_token_refresh",
|
|
|
|
|
msgType: MsgTypeVoiceTokenRefresh,
|
|
|
|
|
payload: `{}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
if cmd.Type() != MsgTypeVoiceTokenRefresh {
|
|
|
|
|
t.Errorf("Type() = %q, want %q", cmd.Type(), MsgTypeVoiceTokenRefresh)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_mute",
|
|
|
|
|
msgType: MsgTypeVoiceMute,
|
|
|
|
|
payload: `{"muted": true}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
vm := cmd.(VoiceMuteCmd)
|
|
|
|
|
if !vm.Muted() {
|
|
|
|
|
t.Error("Muted() = false, want true")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_deafen",
|
|
|
|
|
msgType: MsgTypeVoiceDeafen,
|
|
|
|
|
payload: `{"deafened": true}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
vd := cmd.(VoiceDeafenCmd)
|
|
|
|
|
if !vd.Deafened() {
|
|
|
|
|
t.Error("Deafened() = false, want true")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_camera",
|
|
|
|
|
msgType: MsgTypeVoiceCamera,
|
|
|
|
|
payload: `{"enabled": true}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
vc := cmd.(VoiceCameraCmd)
|
|
|
|
|
if !vc.Enabled() {
|
|
|
|
|
t.Error("Enabled() = false, want true")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_screenshare",
|
|
|
|
|
msgType: MsgTypeVoiceScreenshare,
|
|
|
|
|
payload: `{"enabled": false}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
vs := cmd.(VoiceScreenshareCmd)
|
|
|
|
|
if vs.Enabled() {
|
|
|
|
|
t.Error("Enabled() = true, want false")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_e2ee_announce",
|
|
|
|
|
msgType: MsgTypeVoiceE2EEAnnounce,
|
|
|
|
|
payload: `{"public_key": "dGVzdA=="}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
va := cmd.(VoiceE2EEAnnounceCmd)
|
|
|
|
|
if va.PublicKey() != "dGVzdA==" {
|
|
|
|
|
t.Errorf("PublicKey() = %q, want %q", va.PublicKey(), "dGVzdA==")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "voice_e2ee_offer",
|
|
|
|
|
msgType: MsgTypeVoiceE2EEOffer,
|
|
|
|
|
payload: `{"target_user_id": 99, "encrypted_key": "abc", "iv": "def"}`,
|
|
|
|
|
checkFn: func(t *testing.T, cmd Command) {
|
|
|
|
|
vo := cmd.(VoiceE2EEOfferCmd)
|
|
|
|
|
if vo.TargetUserID() != 99 {
|
|
|
|
|
t.Errorf("TargetUserID() = %d, want 99", vo.TargetUserID())
|
|
|
|
|
}
|
|
|
|
|
if vo.EncryptedKey() != "abc" {
|
|
|
|
|
t.Errorf("EncryptedKey() = %q, want %q", vo.EncryptedKey(), "abc")
|
|
|
|
|
}
|
|
|
|
|
if vo.IV() != "def" {
|
|
|
|
|
t.Errorf("IV() = %q, want %q", vo.IV(), "def")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
ctor, ok := commandConstructors[tt.msgType]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("no constructor for %q", tt.msgType)
|
|
|
|
|
}
|
|
|
|
|
cmd, err := ctor(42, "req-1", json.RawMessage(tt.payload))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cmd.UserID() != 42 {
|
|
|
|
|
t.Errorf("UserID() = %d, want 42", cmd.UserID())
|
|
|
|
|
}
|
|
|
|
|
tt.checkFn(t, cmd)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandConstructorRejectsInvalidJSON(t *testing.T) {
|
|
|
|
|
// All constructors that parse a payload should reject garbage JSON.
|
|
|
|
|
typesWithPayload := []string{
|
|
|
|
|
MsgTypeChatSend,
|
|
|
|
|
MsgTypeChatEdit,
|
|
|
|
|
MsgTypeChatDelete,
|
|
|
|
|
MsgTypeTypingStart,
|
|
|
|
|
MsgTypePresenceUpdate,
|
|
|
|
|
MsgTypeChannelFocus,
|
2026-08-01 22:06:14 +02:00
|
|
|
MsgTypeMarkRead,
|
2026-04-05 19:03:22 +02:00
|
|
|
MsgTypeReactionAdd,
|
|
|
|
|
MsgTypeReactionRemove,
|
|
|
|
|
MsgTypeVoiceJoin,
|
|
|
|
|
MsgTypeVoiceMute,
|
|
|
|
|
MsgTypeVoiceDeafen,
|
|
|
|
|
MsgTypeVoiceCamera,
|
|
|
|
|
MsgTypeVoiceScreenshare,
|
|
|
|
|
MsgTypeVoiceE2EEAnnounce,
|
|
|
|
|
MsgTypeVoiceE2EEOffer,
|
|
|
|
|
}
|
|
|
|
|
badJSON := json.RawMessage(`{not valid json`)
|
|
|
|
|
for _, msgType := range typesWithPayload {
|
|
|
|
|
t.Run(msgType, func(t *testing.T) {
|
|
|
|
|
ctor := commandConstructors[msgType]
|
|
|
|
|
_, err := ctor(1, "req-1", badJSON)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("expected error for invalid JSON, got nil")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandConstructorNoPayloadTypes(t *testing.T) {
|
|
|
|
|
// These types ignore the payload — they should succeed with nil.
|
|
|
|
|
noPayloadTypes := []string{
|
|
|
|
|
MsgTypePing,
|
|
|
|
|
MsgTypeVoiceLeave,
|
|
|
|
|
MsgTypeVoiceTokenRefresh,
|
|
|
|
|
}
|
|
|
|
|
for _, msgType := range noPayloadTypes {
|
|
|
|
|
t.Run(msgType, func(t *testing.T) {
|
|
|
|
|
ctor := commandConstructors[msgType]
|
|
|
|
|
cmd, err := ctor(5, "", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cmd.UserID() != 5 {
|
|
|
|
|
t.Errorf("UserID() = %d, want 5", cmd.UserID())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandChatSendReqID(t *testing.T) {
|
|
|
|
|
ctor := commandConstructors[MsgTypeChatSend]
|
|
|
|
|
cmd, err := ctor(1, "abc-123", json.RawMessage(`{"channel_id": 1, "content": "hi"}`))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
cs := cmd.(ChatSendCmd)
|
|
|
|
|
if cs.ReqID() != "abc-123" {
|
|
|
|
|
t.Errorf("ReqID() = %q, want %q", cs.ReqID(), "abc-123")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandChatSendNilReplyTo(t *testing.T) {
|
|
|
|
|
ctor := commandConstructors[MsgTypeChatSend]
|
|
|
|
|
cmd, err := ctor(1, "", json.RawMessage(`{"channel_id": 1, "content": "hi"}`))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
cs := cmd.(ChatSendCmd)
|
|
|
|
|
if cs.ReplyTo() != nil {
|
|
|
|
|
t.Errorf("ReplyTo() = %v, want nil", cs.ReplyTo())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandAttachmentsDefensiveCopy(t *testing.T) {
|
|
|
|
|
cmd := ChatSendCmd{attachments: []string{"a", "b"}}
|
|
|
|
|
got := cmd.Attachments()
|
|
|
|
|
got[0] = "mutated"
|
|
|
|
|
if cmd.attachments[0] == "mutated" {
|
|
|
|
|
t.Error("Attachments() did not return a defensive copy")
|
|
|
|
|
}
|
|
|
|
|
}
|