1481 lines
35 KiB
Go
1481 lines
35 KiB
Go
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
|
|
}
|
|
|
|
type testLoginDeviceStore struct {
|
|
devices map[types.Uid][]types.DeviceDef
|
|
updates int
|
|
err error
|
|
}
|
|
|
|
func (s *testLoginDeviceStore) Update(tenantID types.TenantID, uid types.Uid, oldDeviceID string, dev *types.DeviceDef) error {
|
|
s.updates++
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
if dev != nil {
|
|
dev.TenantID = tenantID
|
|
s.devices[uid] = append(s.devices[uid], *dev)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *testLoginDeviceStore) GetAll(_ types.TenantID, uids ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
|
|
if s.err != nil {
|
|
return nil, 0, s.err
|
|
}
|
|
result := make(map[types.Uid][]types.DeviceDef)
|
|
count := 0
|
|
for _, uid := range uids {
|
|
result[uid] = append([]types.DeviceDef(nil), s.devices[uid]...)
|
|
count += len(result[uid])
|
|
}
|
|
return result, count, nil
|
|
}
|
|
|
|
func (s *testLoginDeviceStore) Delete(_ types.TenantID, uid types.Uid, deviceID string) error {
|
|
devices := s.devices[uid]
|
|
for i, device := range devices {
|
|
if device.DeviceId == deviceID {
|
|
s.devices[uid] = append(devices[:i], devices[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
return types.ErrNotFound
|
|
}
|
|
|
|
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 (s testTenantStore) ListActive(int) ([]types.Tenant, error) {
|
|
if s.err != nil || s.tenant == nil {
|
|
return nil, s.err
|
|
}
|
|
return []types.Tenant{*s.tenant}, nil
|
|
}
|
|
|
|
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 TestLoginDeviceSecurityNoticeOnlyForNewDevice(t *testing.T) {
|
|
uid := types.Uid(42)
|
|
devices := &testLoginDeviceStore{devices: map[types.Uid][]types.DeviceDef{
|
|
uid: {{TenantID: 7, DeviceId: "known-device", Platform: "ios"}},
|
|
}}
|
|
security := &testSecurityStore{}
|
|
oldDevices := store.Devices
|
|
oldSecurity := store.Security
|
|
store.Devices = devices
|
|
store.Security = security
|
|
t.Cleanup(func() {
|
|
store.Devices = oldDevices
|
|
store.Security = oldSecurity
|
|
})
|
|
|
|
known, err := isNewLoginDevice(7, uid, "known-device")
|
|
if err != nil || known {
|
|
t.Fatalf("known device detected as new: new=%v err=%v", known, err)
|
|
}
|
|
newDevice, err := isNewLoginDevice(7, uid, "new-device")
|
|
if err != nil || !newDevice {
|
|
t.Fatalf("new device was not detected: new=%v err=%v", newDevice, err)
|
|
}
|
|
|
|
createLoginDeviceSecurityNotice(7, uid, "new-device", "ios", "UnitTestUA", time.Date(2026, 9, 6, 12, 0, 0, 0, time.UTC))
|
|
if len(security.notifications) != 1 {
|
|
t.Fatalf("expected one security notification, got %d", len(security.notifications))
|
|
}
|
|
notification := security.notifications[0]
|
|
if notification.EventType != "security.login_device" || notification.Title == "" || notification.Action["device_id"] != "new-device" {
|
|
t.Fatalf("unexpected login device notification: %#v", notification)
|
|
}
|
|
}
|
|
|
|
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
|
|
oldInvites := store.RegistrationInvites
|
|
oldValidators := globals.validators
|
|
globals.validators = map[string]credValidator{"tel": {}}
|
|
store.Store = ss
|
|
store.Users = uu
|
|
store.Security = &testSecurityStore{version: 1}
|
|
store.Verifications = nil
|
|
invites := &testRegistrationInviteStore{invite: &types.RegistrationInviteCode{
|
|
Code: "AbCdE", Status: types.RegistrationInviteActive, ExpiresAt: time.Now().Add(time.Hour),
|
|
}}
|
|
store.RegistrationInvites = invites
|
|
setRegistrationSMSConfigForTest(true)
|
|
defer func() {
|
|
store.Store = nil
|
|
store.Users = nil
|
|
store.Security = oldSecurity
|
|
store.Verifications = oldVerifications
|
|
store.RegistrationInvites = oldInvites
|
|
globals.validators = oldValidators
|
|
resetRegistrationSMSConfigForTest()
|
|
ctrl.Finish()
|
|
}()
|
|
|
|
remoteAddr := "192.168.0.1"
|
|
secret := "alice:password"
|
|
authRec := &auth.Rec{
|
|
TenantID: 1,
|
|
Uid: uid,
|
|
AuthLevel: auth.LevelAuth,
|
|
State: types.StateOK,
|
|
}
|
|
ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa)
|
|
ss.EXPECT().GetValidator("tel").Return(registrationTestValidator{})
|
|
// 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) {
|
|
if user.Phone != "+8613800138000" {
|
|
t.Fatalf("unexpected registration phone: %q", user.Phone)
|
|
}
|
|
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",
|
|
Username: "alice",
|
|
Phone: "13800138000",
|
|
Secret: []byte(secret),
|
|
Cred: []MsgCredClient{{
|
|
Method: "tel", Value: "+8613800138000", Response: "AbCdE",
|
|
Params: map[string]any{"challenge_id": "challenge-1"},
|
|
}},
|
|
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))
|
|
}
|
|
if !invites.useRecorded {
|
|
t.Error("registration invite use was not recorded")
|
|
}
|
|
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 username := p["username"].(string); username != "alice" {
|
|
t.Errorf("Username: expected 'alice', found '%s'.", username)
|
|
}
|
|
if desc := p["desc"].(*MsgTopicDesc); desc.Public.(map[string]any)["fn"] != "alice" {
|
|
t.Errorf("Public fn: expected 'alice', found '%v'.", desc.Public)
|
|
}
|
|
} 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)
|
|
}
|
|
}
|