Files
OwnCord/Server/ws/command_test.go
T

434 lines
12 KiB
Go
Raw Normal View History

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,
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},
{"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},
{"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())
}
},
},
{
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,
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")
}
}