| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- package icq_legacy
- import (
- "log/slog"
- "testing"
- "github.com/mk6i/open-oscar-server/config"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- )
- // newTestDispatcher creates a ProtocolDispatcher wired with real version
- // handlers that share the given mock PacketSender. This lets tests verify
- // that the dispatcher routes to the correct handler by asserting on the
- // mock sender's SendToSession calls.
- func newTestDispatcher(t *testing.T, sender PacketSender) *ProtocolDispatcher {
- t.Helper()
- logger := slog.Default()
- svc := newMockLegacyService(t)
- // Create a minimal LegacySessionManager so V5 handler can call GetSession
- // without panicking. The map is empty so GetSession returns nil, which is
- // handled gracefully by the handlers.
- sessions := &LegacySessionManager{
- sessions: make(map[uint32]*LegacySession),
- addrIndex: make(map[string]*LegacySession),
- }
- v1 := NewV1Handler(sessions, svc, sender, logger)
- v2 := NewV2Handler(sessions, svc, sender, NewV2PacketBuilder(), logger)
- v3 := NewV3Handler(sessions, svc, sender, NewV3PacketBuilder(sessions, nil), logger)
- v4 := NewV4Handler(sessions, svc, sender, NewV4PacketBuilder(sessions, nil), logger)
- v5 := NewV5Handler(sessions, svc, sender, NewV5PacketBuilder(sessions, nil), logger)
- cfg := config.ICQLegacyConfig{
- SupportedVersions: []int{3, 4, 5},
- }
- return NewProtocolDispatcher(v1, v2, v3, v4, v5, cfg, logger)
- }
- // ---------------------------------------------------------------------------
- // Task 12.1 — Table-driven tests for ProtocolDispatcher
- // ---------------------------------------------------------------------------
- func TestProtocolDispatcher_SendUserOnline(t *testing.T) {
- tests := []struct {
- name string
- version uint16
- }{
- {"V3", ICQLegacyVersionV3},
- {"V4", ICQLegacyVersionV4},
- {"V5", ICQLegacyVersionV5},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
- // The handler will call SendToSession exactly once.
- sender.EXPECT().
- SendToSession(session, mock.AnythingOfType("[]uint8")).
- Return(nil)
- err := dispatcher.SendUserOnline(session, 67890, 0x00000000)
- assert.NoError(t, err)
- })
- }
- }
- func TestProtocolDispatcher_SendUserOnline_NilSession(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- // nil session should return nil without calling sender
- err := dispatcher.SendUserOnline(nil, 67890, 0x00000000)
- assert.NoError(t, err)
- }
- func TestProtocolDispatcher_SendUserOffline(t *testing.T) {
- tests := []struct {
- name string
- version uint16
- }{
- {"V3", ICQLegacyVersionV3},
- {"V4", ICQLegacyVersionV4},
- {"V5", ICQLegacyVersionV5},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
- sender.EXPECT().
- SendToSession(session, mock.AnythingOfType("[]uint8")).
- Return(nil)
- err := dispatcher.SendUserOffline(session, 67890)
- assert.NoError(t, err)
- })
- }
- }
- func TestProtocolDispatcher_SendStatusChange(t *testing.T) {
- tests := []struct {
- name string
- version uint16
- }{
- {"V3", ICQLegacyVersionV3},
- {"V4", ICQLegacyVersionV4},
- {"V5", ICQLegacyVersionV5},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
- sender.EXPECT().
- SendToSession(session, mock.AnythingOfType("[]uint8")).
- Return(nil)
- err := dispatcher.SendStatusChange(session, 67890, 0x00000001)
- assert.NoError(t, err)
- })
- }
- }
- func TestProtocolDispatcher_SendOnlineMessage(t *testing.T) {
- tests := []struct {
- name string
- version uint16
- }{
- {"V3", ICQLegacyVersionV3},
- {"V4", ICQLegacyVersionV4},
- {"V5", ICQLegacyVersionV5},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
- sender.EXPECT().
- SendToSession(session, mock.AnythingOfType("[]uint8")).
- Return(nil)
- err := dispatcher.SendOnlineMessage(session, 67890, 0x0001, "Hello, world!")
- assert.NoError(t, err)
- })
- }
- }
- // ---------------------------------------------------------------------------
- // Task 12.2 — Property test: protocol dispatcher version routing
- // **Property 6: Protocol dispatcher routes to version-matching handler**
- // For any version V in {1,2,3,4,5} and any dispatch operation, the dispatcher
- // invokes the V-specific handler (verified by mock sender being called).
- // **Validates: Requirements 6.1, 6.4, 6.7, 6.8**
- // ---------------------------------------------------------------------------
- func TestProperty_DispatcherRoutesToVersionHandler(t *testing.T) {
- versions := []uint16{
- ICQLegacyVersionV3,
- ICQLegacyVersionV4,
- ICQLegacyVersionV5,
- }
- type dispatchOp struct {
- name string
- call func(d *ProtocolDispatcher, s *LegacySession) error
- }
- ops := []dispatchOp{
- {
- name: "SendUserOnline",
- call: func(d *ProtocolDispatcher, s *LegacySession) error {
- return d.SendUserOnline(s, 99999, 0x00000000)
- },
- },
- {
- name: "SendUserOffline",
- call: func(d *ProtocolDispatcher, s *LegacySession) error {
- return d.SendUserOffline(s, 99999)
- },
- },
- {
- name: "SendStatusChange",
- call: func(d *ProtocolDispatcher, s *LegacySession) error {
- return d.SendStatusChange(s, 99999, 0x00000001)
- },
- },
- {
- name: "SendOnlineMessage",
- call: func(d *ProtocolDispatcher, s *LegacySession) error {
- return d.SendOnlineMessage(s, 99999, 0x0001, "test message")
- },
- },
- }
- for _, v := range versions {
- for _, op := range ops {
- t.Run(op.name+"/"+versionName(v), func(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- session := newTestLegacySession(12345, legacySessionOptVersion(v))
- // Expect exactly one SendToSession call — proves the
- // correct handler was invoked (each handler builds a
- // packet and sends it via the shared sender).
- sender.EXPECT().
- SendToSession(session, mock.AnythingOfType("[]uint8")).
- Return(nil)
- err := op.call(dispatcher, session)
- assert.NoError(t, err)
- })
- }
- }
- }
- // ---------------------------------------------------------------------------
- // Task 12.3 — Property test: cross-version message content preservation
- // **Property 7: Cross-version message content preservation**
- // For any message content string and type, and any sender/receiver version
- // pair, the message is dispatched without error.
- // **Validates: Requirements 6.5, 6.6**
- // ---------------------------------------------------------------------------
- func TestProperty_CrossVersionMessageContentPreservation(t *testing.T) {
- versions := []uint16{
- ICQLegacyVersionV3,
- ICQLegacyVersionV4,
- ICQLegacyVersionV5,
- }
- messages := []struct {
- msgType uint16
- content string
- }{
- {0x0001, "Hello"},
- {0x0001, ""},
- {0x0001, "A longer message with special chars: !@#$%^&*()"},
- {0x0004, "URL message"},
- {0x000E, "Email express"},
- {0x0001, "Unicode: café résumé naïve"},
- }
- for _, senderVersion := range versions {
- for _, receiverVersion := range versions {
- for _, msg := range messages {
- testName := versionName(senderVersion) + "->" + versionName(receiverVersion) +
- "/type_" + msgTypeName(msg.msgType)
- t.Run(testName, func(t *testing.T) {
- sender := newMockPacketSender(t)
- dispatcher := newTestDispatcher(t, sender)
- // The receiver session has the receiver's version.
- session := newTestLegacySession(12345, legacySessionOptVersion(receiverVersion))
- // Allow the send call — we just verify no error.
- sender.EXPECT().
- SendToSession(session, mock.AnythingOfType("[]uint8")).
- Return(nil)
- // Dispatch the message as if it came from a sender
- // on senderVersion to a receiver on receiverVersion.
- err := dispatcher.SendOnlineMessage(session, 67890, msg.msgType, msg.content)
- assert.NoError(t, err)
- })
- }
- }
- }
- }
- // versionName returns a human-readable name for a protocol version constant.
- func versionName(v uint16) string {
- switch v {
- case ICQLegacyVersionV1:
- return "V1"
- case ICQLegacyVersionV2:
- return "V2"
- case ICQLegacyVersionV3:
- return "V3"
- case ICQLegacyVersionV4:
- return "V4"
- case ICQLegacyVersionV5:
- return "V5"
- default:
- return "Unknown"
- }
- }
- // msgTypeName returns a short label for common ICQ message types.
- func msgTypeName(t uint16) string {
- switch t {
- case 0x0001:
- return "normal"
- case 0x0004:
- return "url"
- case 0x000E:
- return "email"
- default:
- return "other"
- }
- }
|