package adapter import ( "testing" "time" ) func TestUnifiedMessageConstruction(t *testing.T) { t.Parallel() ts := time.Date(2026, 9, 6, 12, 0, 0, 0, time.UTC) msg := UnifiedMessage{ Platform: "telegram", ChannelID: "ch-100", ChannelType: ChannelGroup, SenderID: "u-1", SenderName: "Alice", Sender: Actor{ Role: RoleOwner, UserID: "u-1", DisplayName: "Alice", GroupID: "g-42", }, Content: "hello @bob", ContentType: ContentText, Media: []Media{ {Kind: MediaImage, URL: "https://example.com/a.jpg", Name: "a.jpg", Size: 1024, MIMEType: "image/jpeg"}, }, Mentions: []Mention{ {UserID: "u-2", UserName: "Bob"}, }, ReplyToID: "m-9", ReplyToText: "earlier", GroupID: "g-42", GroupName: "Team", ThreadID: "t-7", MessageID: "m-10", Timestamp: ts, RawData: []byte(`{"id":"m-10"}`), } if !msg.IsGroup() { t.Errorf("IsGroup() = false, want true") } if msg.ChannelType != ChannelGroup { t.Errorf("ChannelType = %q, want %q", msg.ChannelType, ChannelGroup) } if msg.ContentType != ContentText { t.Errorf("ContentType = %q, want %q", msg.ContentType, ContentText) } if len(msg.Media) != 1 || msg.Media[0].Kind != MediaImage { t.Errorf("Media = %+v, want one image", msg.Media) } if len(msg.Mentions) != 1 || msg.Mentions[0].UserID != "u-2" { t.Errorf("Mentions = %+v, want one user u-2", msg.Mentions) } if msg.ThreadID != "t-7" { t.Errorf("ThreadID = %q, want t-7", msg.ThreadID) } if msg.Sender.Role != RoleOwner || !msg.Sender.IsOwner() { t.Errorf("Sender = %+v, want owner role", msg.Sender) } if !msg.Timestamp.Equal(ts) { t.Errorf("Timestamp = %v, want %v", msg.Timestamp, ts) } } func TestUnifiedMessageValidate(t *testing.T) { t.Parallel() tests := []struct { name string msg UnifiedMessage wantErr bool }{ { name: "valid direct text", msg: UnifiedMessage{ChannelType: ChannelDirect, ContentType: ContentText, Content: "hi"}, wantErr: false, }, { name: "valid group text", msg: UnifiedMessage{ ChannelType: ChannelGroup, ContentType: ContentText, GroupID: "g-1", Content: "hi", }, wantErr: false, }, { name: "group missing id", msg: UnifiedMessage{ ChannelType: ChannelGroup, ContentType: ContentText, Content: "hi", }, wantErr: true, }, { name: "invalid channel type", msg: UnifiedMessage{ChannelType: ChannelType("bogus"), ContentType: ContentText}, wantErr: true, }, { name: "invalid content type", msg: UnifiedMessage{ChannelType: ChannelDirect, ContentType: ContentType("bogus")}, wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() err := tt.msg.Validate() if (err != nil) != tt.wantErr { t.Fatalf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestChannelTypeValid(t *testing.T) { t.Parallel() valid := []ChannelType{ChannelDirect, ChannelGroup} for _, ct := range valid { if !ct.Valid() { t.Errorf("ChannelType(%q).Valid() = false, want true", ct) } } if ChannelType("").Valid() { t.Error("empty ChannelType.Valid() = true, want false") } if ChannelType("bogus").Valid() { t.Error("bogus ChannelType.Valid() = true, want false") } } func TestContentTypeValid(t *testing.T) { t.Parallel() valid := []ContentType{ContentText, ContentImage, ContentVoice, ContentFile, ContentAction} for _, ct := range valid { if !ct.Valid() { t.Errorf("ContentType(%q).Valid() = false, want true", ct) } } if ContentType("").Valid() { t.Error("empty ContentType.Valid() = true, want false") } if ContentType("bogus").Valid() { t.Error("bogus ContentType.Valid() = true, want false") } } func TestMentionedSelf(t *testing.T) { t.Parallel() mentions := []Mention{{UserID: "all"}, {UserID: "u-2"}} tests := []struct { name string selfID string mentions []Mention want bool }{ {name: "platform has not provided self id", selfID: "", mentions: mentions, want: false}, {name: "no mentions", selfID: "u-2", mentions: nil, want: false}, {name: "points at someone else", selfID: "u-9", mentions: mentions, want: false}, {name: "points at her", selfID: "u-2", mentions: mentions, want: true}, {name: "at-all marker is not a user id", selfID: "all", mentions: nil, want: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := MentionedSelf(tt.selfID, tt.mentions); got != tt.want { t.Errorf("MentionedSelf(%q, %+v) = %v, want %v", tt.selfID, tt.mentions, got, tt.want) } }) } } func TestAddressedToSelf(t *testing.T) { t.Parallel() tests := []struct { name string msg UnifiedMessage want bool }{ { name: "private chat is always addressed to her", msg: UnifiedMessage{ChannelType: ChannelDirect, SelfID: "u-1"}, want: true, }, { name: "private chat without platform self id stays addressed", msg: UnifiedMessage{ChannelType: ChannelDirect}, want: true, }, { name: "group mentions her", msg: UnifiedMessage{ ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1", Mentions: []Mention{{UserID: "u-1"}}, }, want: true, }, { name: "group mentions someone else", msg: UnifiedMessage{ ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1", Mentions: []Mention{{UserID: "u-2"}}, }, want: false, }, { name: "group mentions all", msg: UnifiedMessage{ ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1", MentionAll: true, Mentions: []Mention{{UserID: "all"}}, }, want: true, }, { name: "group without mention", msg: UnifiedMessage{ChannelType: ChannelGroup, GroupID: "g-1", SelfID: "u-1"}, want: false, }, { name: "group without platform self id is fail-closed", msg: UnifiedMessage{ ChannelType: ChannelGroup, GroupID: "g-1", Mentions: []Mention{{UserID: "u-1"}}, }, want: false, }, { name: "zero value stays fail-closed for adapters that do not wire it", msg: UnifiedMessage{}, want: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.msg.AddressedToSelf(); got != tt.want { t.Errorf("AddressedToSelf() = %v, want %v (msg=%+v)", got, tt.want, tt.msg) } }) } }