package support import ( "context" "errors" "strings" "testing" "time" "git.118im.com/im_team/olons_server/internal/config" "git.118im.com/im_team/olons_server/internal/imclient" "git.118im.com/im_team/olons_server/internal/store" ) type repositoryStub struct { agent *store.CustomerService identity *store.MemberIMIdentity profile *store.MemberIMProfile } func (r *repositoryStub) CustomerServiceByID(context.Context, int64, int64) (*store.CustomerService, error) { return r.agent, nil } func (r *repositoryStub) MemberIMIdentity(context.Context, int64, int64) (*store.MemberIMIdentity, error) { return r.identity, nil } func (r *repositoryStub) MemberIMProfile(context.Context, int64, int64) (*store.MemberIMProfile, error) { if r.profile == nil { return nil, store.ErrResourceNotFound } return r.profile, nil } func (r *repositoryStub) SaveMemberIMIdentity(_ context.Context, identity *store.MemberIMIdentity) (*store.MemberIMIdentity, error) { copy := *identity r.identity = © return r.identity, nil } type imStub struct { friendRequests int loginAttempts int resetPasswords int loginError error profileError error conversation *imclient.Conversation ensureP2PCalls int } func (i *imStub) Ready() bool { return true } func (i *imStub) Config() config.IMConfig { return config.IMConfig{TenantCode: "allrich", HTTPAPIURL: "https://api.118im.com/v1/im", WebSocketURL: "wss://api.118im.com/v0/channels", FileURL: "https://api.118im.com/v0/file", APIKey: "key"} } func (i *imStub) CreateUser(_ context.Context, username, nickname, _ string) (*imclient.CreatedUser, error) { return &imclient.CreatedUser{UserID: imclient.FlexibleID("1001"), Username: username, Nickname: nickname}, nil } func (i *imStub) FindUserByUsername(context.Context, string) (*imclient.CreatedUser, error) { return nil, nil } func (i *imStub) LoginBasic(context.Context, string, string, string, string) (*imclient.Session, error) { i.loginAttempts++ if i.loginError != nil { err := i.loginError i.loginError = nil return nil, err } return &imclient.Session{UserID: "usr_member", Token: "token", ExpiresAt: time.Now().Add(time.Hour)}, nil } func (i *imStub) ResetPassword(context.Context, string, string) error { i.resetPasswords++ return nil } func (i *imStub) UserProfile(context.Context, string, string) (*imclient.UserProfile, error) { if i.profileError != nil { return nil, i.profileError } return &imclient.UserProfile{UserID: "usr_agent", Relationship: "none"}, nil } func (i *imStub) CreateFriendRequest(context.Context, string, string, string) (*imclient.FriendRequest, error) { i.friendRequests++ return &imclient.FriendRequest{RequestID: "frq_1", Status: "accepted"}, nil } func (i *imStub) EnsureP2P(context.Context, string, string, string) (*imclient.Conversation, error) { i.ensureP2PCalls++ if i.conversation != nil { return i.conversation, nil } return &imclient.Conversation{Topic: "p2p-topic", Relationship: "friend", CanSend: true}, nil } func TestOpenSessionAutoAcceptsAndReturnsTopic(t *testing.T) { repository := &repositoryStub{ agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1}, identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_member", IMUsername: "member01"}, } im := &imStub{} service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) repository.identity.IMPasswordCipher, _ = service.encryptCredential("password") session, err := service.OpenSession(context.Background(), 0, 9, 3, SessionRequest{DeviceID: "web-1", Platform: "web"}) if err != nil { t.Fatal(err) } if session.Topic != "p2p-topic" || session.CurrentUserID != "usr_member" || session.Agent.IMUserID != "usr_agent" { t.Fatalf("unexpected session: %+v", session) } if im.friendRequests != 1 { t.Fatalf("friend requests = %d, want 1", im.friendRequests) } } func TestOpenSessionReturnsSameOriginEndpoints(t *testing.T) { repository := &repositoryStub{ agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1}, identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_member", IMUsername: "member01"}, } im := &imStub{} service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) repository.identity.IMPasswordCipher, _ = service.encryptCredential("password") session, err := service.OpenSession(context.Background(), 0, 9, 3, SessionRequest{DeviceID: "web-1", Platform: "web"}) if err != nil { t.Fatal(err) } if session.APIBaseURL != "/v1/im" || session.WebSocketURL != "/v0/channels" || session.FileURL != "/v0/file" { t.Fatalf("browser endpoints must be same-origin paths: %+v", session) } } func TestOpenSessionReturnsPendingWhenPrivateProfileHidesPendingRelationship(t *testing.T) { repository := &repositoryStub{ agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1}, identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_member", IMUsername: "member01"}, } im := &imStub{ profileError: &imclient.Error{Status: 403, Reason: "PERMISSION_DENIED"}, conversation: &imclient.Conversation{Topic: "p2p-topic", Relationship: "pending_outbound", CanSend: false}, } service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) repository.identity.IMPasswordCipher, _ = service.encryptCredential("password") _, err := service.OpenSession(context.Background(), 0, 9, 3, SessionRequest{DeviceID: "web-1", Platform: "web"}) if !errors.Is(err, ErrRelationPending) { t.Fatalf("error = %v, want ErrRelationPending", err) } if im.ensureP2PCalls != 1 || im.friendRequests != 0 { t.Fatalf("ensure p2p calls=%d friend requests=%d, want 1 and 0", im.ensureP2PCalls, im.friendRequests) } } func TestOpenSessionRepairsDriftedIMCredential(t *testing.T) { repository := &repositoryStub{ agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1}, identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_member", IMUsername: "member01"}, } im := &imStub{loginError: &imclient.Error{Status: 401, Reason: "authentication failed"}} service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) repository.identity.IMPasswordCipher, _ = service.encryptCredential("stale-password") session, err := service.OpenSession(context.Background(), 0, 9, 3, SessionRequest{DeviceID: "web-1", Platform: "web"}) if err != nil { t.Fatal(err) } if session.Topic != "p2p-topic" { t.Fatalf("unexpected session: %+v", session) } if im.loginAttempts != 2 || im.resetPasswords != 1 { t.Fatalf("login attempts=%d reset passwords=%d, want 2 and 1", im.loginAttempts, im.resetPasswords) } } func TestMemberIMUsernameMatchesIMConstraint(t *testing.T) { username := memberIMUsername(0, 9223372036854775807, "allrich") if len(username) != 10 { t.Fatalf("username length = %d, want 10: %s", len(username), username) } for _, ch := range username { if (ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') { t.Fatalf("invalid username %q", username) } } } func TestEnsureIdentityReprovisionsMemberWhenIMTenantChanges(t *testing.T) { repository := &repositoryStub{ identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_old", IMUsername: "olduser"}, profile: &store.MemberIMProfile{MemberID: 9, Username: "member01"}, } im := &imStub{} service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) identity, err := service.ensureIdentity(context.Background(), 0, 9, "default") if err != nil { t.Fatal(err) } if identity.IMTenantCode != "default" || identity.IMUserID != "usr_member" || identity.IMUsername != memberIMUsername(0, 9, "default") { t.Fatalf("unexpected migrated identity: %+v", identity) } if im.loginAttempts != 1 { t.Fatalf("provisioning login attempts = %d, want 1", im.loginAttempts) } }