package main import ( "net/http" "sync" "testing" "time" "github.com/golang/mock/gomock" "github.com/tinode/chat/server/auth" "github.com/tinode/chat/server/auth/mock_auth" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/mock_store" "github.com/tinode/chat/server/store/types" ) type testTenantStore struct { tenant *types.Tenant err error } func (s testTenantStore) GetByCode(string) (*types.Tenant, error) { return s.tenant, s.err } func (s testTenantStore) GetByID(types.TenantID) (*types.Tenant, error) { return s.tenant, s.err } func test_makeSession(uid types.Uid) *Session { return &Session{ send: make(chan any, 10), uid: uid, authLvl: auth.LevelAuth, inflightReqs: newBoundedWaitGroup(1), ver: 22, tenantID: 1, tenantCode: "test", } } func TestDispatchHello(t *testing.T) { oldTenants := store.Tenants store.Tenants = testTenantStore{tenant: &types.Tenant{ ID: 7, Code: "acme", Name: "Acme Corp", State: types.TenantStateActive, }} defer func() { store.Tenants = oldTenants }() s := &Session{ send: make(chan any, 10), uid: types.Uid(1), authLvl: auth.LevelAuth, } wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) msg := &ClientComMessage{ Hi: &MsgClientHi{ Id: "123", Version: "1", UserAgent: "test-ua", Lang: "en-GB", Tenant: "acme", }, } s.dispatch(msg) close(s.send) wg.Wait() if len(r.messages) != 1 { t.Errorf("responses: expected 1, received %d.", len(r.messages)) } resp := r.messages[0].(*ServerComMessage) if resp == nil { t.Fatal("Response must be ServerComMessage") } if resp.Ctrl != nil { if resp.Ctrl.Code != 201 { t.Errorf("Response code: expected 201, got %d", resp.Ctrl.Code) } if resp.Ctrl.Params == nil { t.Error("Response is expected to contain params dict.") } } else { t.Error("Response must contain a ctrl message.") } if s.lang != "en-GB" { t.Errorf("Session language expected to be 'en-GB' vs '%s'", s.lang) } if s.userAgent != "test-ua" { t.Errorf("Session UA expected to be 'test-ua' vs '%s'", s.userAgent) } if s.countryCode != "GB" { t.Errorf("Country code expected to be 'GB' vs '%s'", s.countryCode) } if s.ver == 0 { t.Errorf("s.ver expected 0 vs found %d", s.ver) } if s.tenantID != 7 || s.tenantCode != "acme" { t.Errorf("unexpected tenant binding: id=%d code=%q", s.tenantID, s.tenantCode) } tenantParams := resp.Ctrl.Params.(map[string]any)["tenant"].(map[string]any) if tenantParams["code"] != "acme" || tenantParams["name"] != "Acme Corp" { t.Errorf("unexpected public tenant params: %#v", tenantParams) } } func TestDispatchHelloDefaultsTenant(t *testing.T) { oldTenants := store.Tenants store.Tenants = testTenantStore{tenant: &types.Tenant{ ID: 1, Code: "default", Name: "Default Tenant", State: types.TenantStateActive, }} defer func() { store.Tenants = oldTenants }() s := &Session{send: make(chan any, 1)} s.dispatch(&ClientComMessage{Hi: &MsgClientHi{Id: "123", Version: "1"}}) resp := (<-s.send).(*ServerComMessage) if resp.Ctrl.Code != http.StatusCreated { t.Fatalf("response code: expected %d, got %d", http.StatusCreated, resp.Ctrl.Code) } if s.tenantID != 1 || s.tenantCode != "default" || s.ver == 0 { t.Fatalf("default hello did not bind session: tenant=%d code=%q version=%d", s.tenantID, s.tenantCode, s.ver) } tenantParams := resp.Ctrl.Params.(map[string]any)["tenant"].(map[string]any) if tenantParams["code"] != "default" || tenantParams["name"] != "Default Tenant" { t.Fatalf("unexpected tenant params: %#v", tenantParams) } } func TestDispatchHelloRejectsInactiveTenant(t *testing.T) { oldTenants := store.Tenants store.Tenants = testTenantStore{tenant: &types.Tenant{ ID: 7, Code: "acme", Name: "Acme Corp", State: types.TenantStateSuspended, }} defer func() { store.Tenants = oldTenants }() s := &Session{send: make(chan any, 1)} s.dispatch(&ClientComMessage{Hi: &MsgClientHi{Id: "123", Version: "1", Tenant: "acme"}}) resp := (<-s.send).(*ServerComMessage) if resp.Ctrl.Code != http.StatusForbidden { t.Fatalf("response code: expected %d, got %d", http.StatusForbidden, resp.Ctrl.Code) } if !s.tenantID.IsZero() || s.ver != 0 { t.Fatalf("inactive tenant must not bind session: tenant=%d version=%d", s.tenantID, s.ver) } } func TestDispatchHelloRejectsTenantChange(t *testing.T) { s := &Session{ send: make(chan any, 1), uid: 1, ver: parseVersion("1"), tenantID: 7, tenantCode: "acme", } s.dispatch(&ClientComMessage{Hi: &MsgClientHi{Id: "123", Version: "1", Tenant: "other"}}) resp := (<-s.send).(*ServerComMessage) if resp.Ctrl.Code != http.StatusConflict { t.Fatalf("response code: expected %d, got %d", http.StatusConflict, resp.Ctrl.Code) } if s.tenantID != 7 || s.tenantCode != "acme" { t.Fatalf("tenant binding changed: tenant=%d code=%q", s.tenantID, s.tenantCode) } } func verifyResponseCodes(r *responses, codes []int, t *testing.T) { if len(r.messages) != len(codes) { t.Errorf("responses: expected %d, received %d.", len(codes), len(r.messages)) } for i := range codes { resp := r.messages[i].(*ServerComMessage) if resp == nil { t.Fatalf("Response %d must be ServerComMessage", i) } if resp.Ctrl == nil { t.Fatalf("Response %d must contain a ctrl message.", i) } if resp.Ctrl.Code != codes[i] { t.Errorf("Response code: expected %d, got %d", codes[i], resp.Ctrl.Code) } } } func TestDispatchInvalidVersion(t *testing.T) { s := &Session{ send: make(chan any, 10), uid: types.Uid(1), authLvl: auth.LevelAuth, } wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) msg := &ClientComMessage{ Hi: &MsgClientHi{ Id: "123", // Invalid version string. Version: "INVALID VERSION STRING", }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusBadRequest}, t) } func TestDispatchUnsupportedVersion(t *testing.T) { s := &Session{ send: make(chan any, 10), uid: types.Uid(1), authLvl: auth.LevelAuth, } wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) msg := &ClientComMessage{ Hi: &MsgClientHi{ Id: "123", // Invalid version string. Version: "0.1", }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusHTTPVersionNotSupported}, t) } func TestDispatchLogin(t *testing.T) { ctrl := gomock.NewController(t) ss := mock_store.NewMockPersistentStorageInterface(ctrl) aa := mock_auth.NewMockAuthHandler(ctrl) uid := types.Uid(1) oldSecurity := store.Security store.Store = ss store.Security = &testSecurityStore{version: 1} defer func() { store.Store = nil store.Security = oldSecurity ctrl.Finish() }() secret := "<==auth-secret==>" authRec := &auth.Rec{ TenantID: 1, Uid: uid, AuthLevel: auth.LevelAuth, Tags: []string{"tag1", "tag2"}, State: types.StateOK, } ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa) aa.EXPECT().Authenticate(auth.AuthContext{TenantID: 1}, []byte(secret)).Return(authRec, nil, nil) // Token generation. ss.EXPECT().GetLogicalAuthHandler("token").Return(aa) token := "<==auth-token==>" expires, _ := time.Parse(time.RFC822, "01 Jan 50 00:00 UTC") aa.EXPECT().GenSecret(authRec).Return([]byte(token), expires, nil) s := &Session{ send: make(chan any, 10), authLvl: auth.LevelAuth, ver: 16, tenantID: 1, tenantCode: "test", } wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) msg := &ClientComMessage{ Login: &MsgClientLogin{ Id: "123", Scheme: "basic", Secret: []byte(secret), }, } s.dispatch(msg) close(s.send) wg.Wait() if len(r.messages) != 1 { t.Errorf("responses: expected 1, received %d.", len(r.messages)) } resp := r.messages[0].(*ServerComMessage) if resp == nil { t.Fatal("Response must be ServerComMessage") } if resp.Ctrl != nil { if resp.Ctrl.Id != "123" { t.Errorf("Response id: expected '123', found '%s'", resp.Ctrl.Id) } if resp.Ctrl.Code != 200 { t.Errorf("Response code: expected 200, got %d", resp.Ctrl.Code) } if resp.Ctrl.Params == nil { t.Error("Response is expected to contain params dict.") } p := resp.Ctrl.Params.(map[string]any) if authToken := string(p["token"].([]byte)); authToken != token { t.Errorf("Auth token: expected '%s', found '%s'.", token, authToken) } if exp := p["expires"].(time.Time); exp != expires { t.Errorf("Token expiration: expected '%s', found '%s'.", expires, exp) } } else { t.Error("Response must contain a ctrl message.") } } func TestDispatchLoginRejectsAuthenticatorTenantMismatch(t *testing.T) { ctrl := gomock.NewController(t) ss := mock_store.NewMockPersistentStorageInterface(ctrl) aa := mock_auth.NewMockAuthHandler(ctrl) store.Store = ss defer func() { store.Store = nil ctrl.Finish() }() secret := []byte("alice:password") ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa) aa.EXPECT().Authenticate(auth.AuthContext{TenantID: 1}, secret).Return(&auth.Rec{ TenantID: 2, Uid: 42, AuthLevel: auth.LevelAuth, State: types.StateOK, }, nil, nil) s := &Session{send: make(chan any, 1), ver: 16, tenantID: 1, tenantCode: "acme"} s.login(&ClientComMessage{ Login: &MsgClientLogin{Id: "login-1", Scheme: "basic", Secret: secret}, Timestamp: time.Now(), }) resp := (<-s.send).(*ServerComMessage) if resp.Ctrl == nil || resp.Ctrl.Code != http.StatusUnauthorized { t.Fatalf("unexpected response: %+v", resp) } if !s.uid.IsZero() { t.Fatalf("tenant mismatch authenticated session as %s", s.uid.UserId()) } } func TestPublishSysRequiresRoot(t *testing.T) { s := &Session{ send: make(chan any, 1), uid: types.Uid(42), authLvl: auth.LevelAuth, tenantID: 1, tenantCode: "test", } s.publish(&ClientComMessage{ Original: "sys", RcptTo: "sys", AsUser: types.Uid(42).UserId(), Timestamp: time.Now(), Pub: &MsgClientPub{Id: "pub-1", Topic: "sys", Content: map[string]any{"type": "system"}}, }) resp := (<-s.send).(*ServerComMessage) if resp.Ctrl == nil || resp.Ctrl.Code != http.StatusForbidden { t.Fatalf("unexpected response: %+v", resp) } } func TestDispatchRejectsCrossTenantAsUser(t *testing.T) { ctrl := gomock.NewController(t) users := mock_store.NewMockUsersPersistenceInterface(ctrl) oldUsers := store.Users store.Users = users defer func() { store.Users = oldUsers ctrl.Finish() }() target := types.Uid(42) users.EXPECT().Get(types.TenantID(1), target).Return(&types.User{TenantID: 2, State: types.StateOK}, nil) s := &Session{ send: make(chan any, 1), uid: 1, authLvl: auth.LevelRoot, ver: 16, tenantID: 1, tenantCode: "acme", } s.dispatch(&ClientComMessage{Extra: &MsgClientExtra{AsUser: target.UserId()}}) resp := (<-s.send).(*ServerComMessage) if resp.Ctrl == nil || resp.Ctrl.Code != http.StatusForbidden { t.Fatalf("unexpected response: %+v", resp) } } func TestDispatchSubscribe(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) hub := &Hub{ join: make(chan *ClientComMessage, 10), } globals.hub = hub defer func() { globals.hub = nil }() msg := &ClientComMessage{ Sub: &MsgClientSub{ Id: "123", Topic: "me", Get: &MsgGetQuery{ What: "sub desc tags cred", }, }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the hub. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(hub.join) == 1 { join := <-hub.join if join.sess != s { t.Error("Hub.join request: sess field expected to be the session under test.") } if join != msg { t.Error("Hub.join request: subscribe message expected to be the original subscribe message.") } } else { t.Errorf("Hub join messages: expected 1, received %d.", len(hub.join)) } s.inflightReqs.Done() } func TestDispatchAlreadySubscribed(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) msg := &ClientComMessage{ Sub: &MsgClientSub{ Id: "123", Topic: "me", Get: &MsgGetQuery{ What: "sub desc tags cred", }, }, } // Pretend the session's already subscribed to topic 'me'. s.subs = make(map[string]*Subscription) s.subs[uid.UserId()] = &Subscription{} s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusNotModified}, t) } func TestDispatchSubscribeJoinChannelFull(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) hub := &Hub{ // Make it unbuffered with no readers - so emit operation fails immediately. join: make(chan *ClientComMessage), } globals.hub = hub defer func() { globals.hub = nil }() msg := &ClientComMessage{ Sub: &MsgClientSub{ Id: "123", Topic: "me", Get: &MsgGetQuery{ What: "sub desc tags cred", }, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusServiceUnavailable}, t) } func TestDispatchLeave(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) leave := make(chan *ClientComMessage, 1) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ done: leave, } msg := &ClientComMessage{ Leave: &MsgClientLeave{ Id: "123", Topic: destUid.UserId(), }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the leave channel. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(leave) == 1 { req := <-leave if req.sess != s { t.Error("Leave request: sess field expected to be the session under test.") } if req != msg { t.Error("Leave request: leave message expected to be the original leave message.") } // leave request handler is expected to clean up subs. s.delSub(topicName) } else { t.Errorf("Unsub messages: expected 1, received %d.", len(leave)) } if len(s.subs) != 0 { t.Errorf("Session subs: expected to be empty, actual size: %d", len(s.subs)) } s.inflightReqs.Done() } func TestDispatchLeaveUnsubMe(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) s.subs = make(map[string]*Subscription) s.subs[uid.UserId()] = &Subscription{} msg := &ClientComMessage{ Leave: &MsgClientLeave{ Id: "123", // Cannot unsubscribe from 'me'. Topic: "me", Unsub: true, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusForbidden}, t) } func TestDispatchLeaveUnknownTopic(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) // Session isn't subscribed to topic 'me'. // And wants to leave it => no change. s.subs = make(map[string]*Subscription) msg := &ClientComMessage{ Leave: &MsgClientLeave{ Id: "123", Topic: "me", }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusNotModified}, t) } func TestDispatchLeaveUnsubFromUnknownTopic(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) // Session isn't subscribed to topic 'me'. // And wants to leave & unsubscribe from it. s.subs = make(map[string]*Subscription) msg := &ClientComMessage{ Leave: &MsgClientLeave{ Id: "123", Topic: "me", Unsub: true, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusConflict}, t) } func TestDispatchPublish(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) brdcst := make(chan *ClientComMessage, 1) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ broadcast: brdcst, } testMessage := "test content" msg := &ClientComMessage{ Pub: &MsgClientPub{ Id: "123", Topic: destUid.UserId(), Content: testMessage, }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the broadcast channel. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(brdcst) == 1 { req := <-brdcst if req.sess != s { t.Error("Pub request: sess field expected to be the session under test.") } if req.Pub.Content != testMessage { t.Errorf("Pub request content: expected '%s' vs '%s'.", testMessage, req.Pub.Content) } if req.Pub.Topic != destUid.UserId() { t.Errorf("Pub request topic: expected '%s' vs '%s'.", destUid.UserId(), req.Pub.Topic) } } else { t.Errorf("Pub messages: expected 1, received %d.", len(brdcst)) } } func TestDispatchPublishBroadcastChannelFull(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) // Make broadcast channel unbuffered with no reader - // emit op will fail. brdcst := make(chan *ClientComMessage) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ broadcast: brdcst, } testMessage := "test content" msg := &ClientComMessage{ Pub: &MsgClientPub{ Id: "123", Topic: destUid.UserId(), Content: testMessage, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusServiceUnavailable}, t) } func TestDispatchPublishMissingSubcription(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) // Subscription to topic missing. s.subs = make(map[string]*Subscription) testMessage := "test content" msg := &ClientComMessage{ Pub: &MsgClientPub{ Id: "123", Topic: destUid.UserId(), Content: testMessage, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusConflict}, t) } func TestDispatchGet(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) meta := make(chan *ClientComMessage, 1) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ meta: meta, } msg := &ClientComMessage{ Get: &MsgClientGet{ Id: "123", Topic: destUid.UserId(), MsgGetQuery: MsgGetQuery{ What: "desc sub del cred", }, }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the meta channel. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(meta) == 1 { req := <-meta if req.sess != s { t.Error("Get request: sess field expected to be the session under test.") } } else { t.Errorf("Get messages: expected 1, received %d.", len(meta)) } } func TestDispatchGetMalformedWhat(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) msg := &ClientComMessage{ Get: &MsgClientGet{ Id: "123", Topic: destUid.UserId(), MsgGetQuery: MsgGetQuery{ // Empty 'what'. This will produce an error. What: "", }, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusBadRequest}, t) } func TestDispatchGetMetaChannelFull(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) // Unbuffered chan with no readers - emit will fail. meta := make(chan *ClientComMessage) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ meta: meta, } msg := &ClientComMessage{ Get: &MsgClientGet{ Id: "123", Topic: destUid.UserId(), MsgGetQuery: MsgGetQuery{ What: "desc sub", }, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusServiceUnavailable}, t) } func TestDispatchSet(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) meta := make(chan *ClientComMessage, 1) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ meta: meta, } msg := &ClientComMessage{ Set: &MsgClientSet{ Id: "123", Topic: destUid.UserId(), MsgSetQuery: MsgSetQuery{ Desc: &MsgSetDesc{}, Sub: &MsgSetSub{}, Tags: []string{"abc"}, Cred: &MsgCredClient{}, }, }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the meta channel. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(meta) == 1 { req := <-meta if req.sess != s { t.Error("Set request: sess field expected to be the session under test.") } expectedWhat := constMsgMetaDesc | constMsgMetaSub | constMsgMetaTags | constMsgMetaCred if msg.MetaWhat != expectedWhat { t.Errorf("Set request what: expected %d vs %d", expectedWhat, msg.MetaWhat) } } else { t.Errorf("Set messages: expected 1, received %d.", len(meta)) } } func TestDispatchSetMalformedWhat(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) msg := &ClientComMessage{ Set: &MsgClientSet{ Id: "123", Topic: destUid.UserId(), MsgSetQuery: MsgSetQuery{ // No meta requests. }, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusBadRequest}, t) } func TestDispatchSetMetaChannelFull(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) // Unbuffered meta channel w/ no readers - emit will fail. meta := make(chan *ClientComMessage) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ meta: meta, } msg := &ClientComMessage{ Set: &MsgClientSet{ Id: "123", Topic: destUid.UserId(), MsgSetQuery: MsgSetQuery{ // No meta requests. Desc: &MsgSetDesc{}, Sub: &MsgSetSub{}, Tags: []string{"abc"}, Cred: &MsgCredClient{}, }, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusServiceUnavailable}, t) } func TestDispatchDelMsg(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) meta := make(chan *ClientComMessage, 1) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ meta: meta, } msg := &ClientComMessage{ Del: &MsgClientDel{ Id: "123", Topic: destUid.UserId(), What: "msg", DelSeq: []MsgRange{{LowId: 3, HiId: 4}}, Hard: true, }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the meta channel. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(meta) == 1 { req := <-meta if req.sess != s { t.Error("Del request: sess field expected to be the session under test.") } } else { t.Errorf("Del messages: expected 1, received %d.", len(meta)) } } func TestDispatchDelMalformedWhat(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) msg := &ClientComMessage{ Del: &MsgClientDel{ Id: "123", Topic: destUid.UserId(), // Invalid 'what' - this will produce an error. What: "INVALID", }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusBadRequest}, t) } func TestDispatchDelMetaChanFull(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) // Unbuffered chan - to simulate a full buffered chan. meta := make(chan *ClientComMessage) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ meta: meta, } msg := &ClientComMessage{ Del: &MsgClientDel{ Id: "123", Topic: destUid.UserId(), What: "msg", DelSeq: []MsgRange{{LowId: 3, HiId: 4}}, Hard: true, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusServiceUnavailable}, t) } func TestDispatchDelUnsubscribedSession(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) // Session isn't subscribed. s.subs = make(map[string]*Subscription) msg := &ClientComMessage{ Del: &MsgClientDel{ Id: "123", Topic: destUid.UserId(), What: "msg", DelSeq: []MsgRange{{LowId: 3, HiId: 4}}, Hard: true, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusConflict}, t) } func TestDispatchNote(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) brdcst := make(chan *ClientComMessage, 1) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ broadcast: brdcst, } msg := &ClientComMessage{ Note: &MsgClientNote{ Topic: destUid.UserId(), What: "recv", SeqId: 5, }, } s.dispatch(msg) close(s.send) wg.Wait() // Check we've routed the join request via the broadcast channel. if len(r.messages) != 0 { t.Errorf("responses: expected 0, received %d.", len(r.messages)) } if len(brdcst) == 1 { req := <-brdcst if req.sess != s { t.Error("Pub request: sess field expected to be the session under test.") } if req.Note.What != msg.Note.What { t.Errorf("Note request what: expected '%s' vs '%s'.", msg.Note.What, req.Note.What) } if req.Note.SeqId != msg.Note.SeqId { t.Errorf("Note request seqId: expected %d vs %d.", msg.Note.SeqId, req.Note.SeqId) } if req.Note.Topic != destUid.UserId() { t.Errorf("Note request topic: expected '%s' vs '%s'.", destUid.UserId(), req.Note.Topic) } } else { t.Errorf("Note messages: expected 1, received %d.", len(brdcst)) } } func TestDispatchNoteBroadcastChanFull(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) topicName := uid.P2PName(destUid) // Unbuffered chan - to simulate a full buffered chan. brdcst := make(chan *ClientComMessage) s.subs = make(map[string]*Subscription) s.subs[topicName] = &Subscription{ broadcast: brdcst, } msg := &ClientComMessage{ Note: &MsgClientNote{ Topic: destUid.UserId(), What: "recv", SeqId: 5, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusServiceUnavailable}, t) } func TestDispatchNoteOnNonSubscribedTopic(t *testing.T) { uid := types.Uid(1) s := test_makeSession(uid) wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) destUid := types.Uid(2) s.subs = make(map[string]*Subscription) msg := &ClientComMessage{ Note: &MsgClientNote{ Topic: destUid.UserId(), What: "read", SeqId: 5, }, } s.dispatch(msg) close(s.send) wg.Wait() verifyResponseCodes(&r, []int{http.StatusConflict}, t) } func TestDispatchAccNew(t *testing.T) { ctrl := gomock.NewController(t) ss := mock_store.NewMockPersistentStorageInterface(ctrl) uu := mock_store.NewMockUsersPersistenceInterface(ctrl) aa := mock_auth.NewMockAuthHandler(ctrl) uid := types.Uid(1) oldSecurity := store.Security oldVerifications := store.Verifications store.Store = ss store.Users = uu store.Security = &testSecurityStore{version: 1} store.Verifications = nil defer func() { store.Store = nil store.Users = nil store.Security = oldSecurity store.Verifications = oldVerifications ctrl.Finish() }() remoteAddr := "192.168.0.1" secret := "<==auth-secret==>" authRec := &auth.Rec{ TenantID: 1, Uid: uid, AuthLevel: auth.LevelAuth, State: types.StateOK, } ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa) // This login is available. aa.EXPECT().IsUnique(auth.AuthContext{TenantID: 1, RemoteAddr: remoteAddr}, []byte(secret)).Return(true, nil) uu.EXPECT().Create(types.TenantID(1), gomock.Any(), gomock.Any()).DoAndReturn( func(_ types.TenantID, user *types.User, private any) (*types.User, error) { user.SetUid(uid) return user, nil }) aa.EXPECT().AddRecord(auth.AuthContext{TenantID: 1, RemoteAddr: remoteAddr}, gomock.Any(), []byte(secret)).Return(authRec, nil) // Token generation. ss.EXPECT().GetLogicalAuthHandler("token").Return(aa) token := "<==auth-token==>" aa.EXPECT().GenSecret(gomock.Any()).Return([]byte(token), time.Now(), nil) s := &Session{ send: make(chan any, 10), authLvl: auth.LevelAuth, ver: 16, tenantID: 1, tenantCode: "test", remoteAddr: remoteAddr, } wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) public := "public name" msg := &ClientComMessage{ Acc: &MsgClientAcc{ Id: "123", User: "newXYZ", Scheme: "basic", Secret: []byte(secret), Desc: &MsgSetDesc{Public: public}, }, } s.dispatch(msg) close(s.send) wg.Wait() if len(r.messages) != 1 { t.Errorf("responses: expected 1, received %d.", len(r.messages)) } resp := r.messages[0].(*ServerComMessage) if resp == nil { t.Fatal("Response must be ServerComMessage") } if resp.Ctrl != nil { if resp.Ctrl.Id != "123" { t.Errorf("Response id: expected '123', found '%s'", resp.Ctrl.Id) } if resp.Ctrl.Code != 201 { t.Errorf("Response code: expected 201, got %d", resp.Ctrl.Code) } if resp.Ctrl.Params == nil { t.Error("Response is expected to contain params dict.") } p := resp.Ctrl.Params.(map[string]any) if respUid := string(p["user"].(string)); respUid != uid.UserId() { t.Errorf("Response uid: expected '%s', found '%s'.", uid.UserId(), respUid) } if lvl := p["authlvl"].(string); lvl != auth.LevelAuth.String() { t.Errorf("Auth level: expected '%s', found '%s'.", auth.LevelAuth.String(), lvl) } if desc := p["desc"].(*MsgTopicDesc); desc.Public.(string) != public { t.Errorf("Public: expected '%s', found '%s'.", public, desc.Public.(string)) } } else { t.Error("Response must contain a ctrl message.") } } func TestDispatchNoMessage(t *testing.T) { remoteAddr := "192.168.0.1" s := &Session{ send: make(chan any, 10), authLvl: auth.LevelAuth, ver: 16, tenantID: 1, tenantCode: "test", remoteAddr: remoteAddr, } wg := sync.WaitGroup{} r := responses{} wg.Add(1) go s.testWriteLoop(&r, &wg) msg := &ClientComMessage{} s.dispatch(msg) close(s.send) wg.Wait() if len(r.messages) != 1 { t.Errorf("responses: expected 1, received %d.", len(r.messages)) } resp := r.messages[0].(*ServerComMessage) if resp == nil { t.Fatal("Response must be ServerComMessage") } if resp.Ctrl == nil { t.Fatal("Response must contain a ctrl message.") } if resp.Ctrl.Code != 400 { t.Errorf("Response code: expected 400, got %d", resp.Ctrl.Code) } }