Files
im_server/server/user_registration_test.go
T

225 lines
8.8 KiB
Go

package main
import (
"regexp"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/mock_store"
"github.com/tinode/chat/server/store/types"
)
type registrationTestValidator struct{}
func (registrationTestValidator) Init(string) error { return nil }
func (registrationTestValidator) IsInitialized() bool { return true }
func (registrationTestValidator) PreCheck(value string, _ map[string]any) (string, error) {
return "tel:" + value, nil
}
func (registrationTestValidator) Request(types.TenantID, types.Uid, string, string, string, []byte) (bool, error) {
return true, nil
}
func (registrationTestValidator) ResetSecret(types.TenantID, string, string, string, []byte, map[string]any) error {
return nil
}
func (registrationTestValidator) Check(types.TenantID, types.Uid, string) (string, error) {
return "", nil
}
func (registrationTestValidator) Remove(types.TenantID, types.Uid, string) error { return nil }
func (registrationTestValidator) Delete(types.TenantID, types.Uid) error { return nil }
func (registrationTestValidator) TempAuthScheme() (string, error) { return "code", nil }
func TestResolveRegistrationAccount(t *testing.T) {
tests := []struct {
name string
username string
phone string
wantAccount string
wantPhone string
wantSecret string
wantPhoneFlow bool
wantGenerated bool
wantErr bool
}{
{name: "username", username: "alice1", wantAccount: "alice1", wantSecret: "alice1"},
{name: "numeric username", username: "12345", wantAccount: "12345", wantSecret: "12345"},
{name: "username and phone", username: "alice1", phone: "13800138000", wantAccount: "alice1", wantPhone: "+8613800138000", wantSecret: "alice1", wantPhoneFlow: true},
{name: "phone only", phone: "13800138000", wantPhone: "+8613800138000", wantSecret: "+8613800138000", wantPhoneFlow: true, wantGenerated: true},
{name: "uppercase with phone rejected", username: "Alice", phone: "13800138000", wantErr: true},
{name: "uppercase rejected", username: "Alice", wantErr: true},
{name: "long username rejected", username: "abcdefghijk", wantErr: true},
{name: "malformed phone does not fall back", username: "alice", phone: "1380013800", wantErr: true},
{name: "phone whitespace rejected", username: "alice", phone: " 13800138000", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
account, phone, secretAccount, phoneFlow, err := resolveRegistrationAccount(tc.username, tc.phone)
if (err != nil) != tc.wantErr || phone != tc.wantPhone || secretAccount != tc.wantSecret || phoneFlow != tc.wantPhoneFlow {
t.Fatalf("got account=%q phone=%q secret=%q phoneFlow=%v err=%v", account, phone, secretAccount, phoneFlow, err)
}
if tc.wantGenerated {
if !regexp.MustCompile(`^[a-z]{3}[0-9]{6}$`).MatchString(account) {
t.Fatalf("generated account %q does not match expected format", account)
}
} else if account != tc.wantAccount {
t.Fatalf("got account=%q want %q", account, tc.wantAccount)
}
})
}
}
func TestRegistrationSecretMatchesSelectedAccount(t *testing.T) {
if !registrationSecretMatches([]byte("alice:password:with-colon"), "alice") {
t.Fatal("matching account was rejected")
}
if registrationSecretMatches([]byte("bob:password"), "alice") || registrationSecretMatches([]byte("password"), "alice") {
t.Fatal("mismatched or malformed secret was accepted")
}
}
func TestRegistrationSecretForAccount(t *testing.T) {
if got := string(registrationSecretForAccount([]byte("+8613800138000:secret"), "abcxyz123456")); got != "abcxyz123456:secret" {
t.Fatalf("got %q", got)
}
if got := string(registrationSecretForAccount([]byte("13800138000:secret"), "abcxyz123456")); got != "abcxyz123456:secret" {
t.Fatalf("got %q", got)
}
}
func TestValidateAccountRegistrationTenantDefaultsEnterpriseFromSession(t *testing.T) {
msg := &ClientComMessage{Acc: &MsgClientAcc{}}
s := &Session{tenantCode: "system"}
if err := validateAccountRegistrationTenant(s, msg); err != nil {
t.Fatalf("validateAccountRegistrationTenant returned %v", err)
}
if msg.Acc.RegistrationType == nil || *msg.Acc.RegistrationType != registrationTypeEnterprise {
t.Fatalf("registration type was not defaulted to enterprise: %#v", msg.Acc.RegistrationType)
}
if msg.Acc.TenantCode != "system" {
t.Fatalf("tenant code default = %q, want system", msg.Acc.TenantCode)
}
}
func TestValidateAccountRegistrationTenantStillRequiresPersonalType(t *testing.T) {
err := validateAccountRegistrationTenant(&Session{tenantCode: imDefaultTenantCode}, &ClientComMessage{Acc: &MsgClientAcc{}})
if err != errRegistrationTypeRequired {
t.Fatalf("got %v, want registration type required", err)
}
}
func TestAttachLegacyRegistrationSMSChallenge(t *testing.T) {
oldVerifications := store.Verifications
defer func() {
store.Verifications = oldVerifications
}()
tenantID := types.TenantID(7)
phone := "+8618612345678"
code := "123456"
phoneHash := hashIMSecret(tenantID, "register", phone, "phone")
challengeID := "challenge-legacy"
store.Verifications = &testVerificationStore{active: &types.VerificationCode{
TenantID: tenantID,
ChallengeID: challengeID,
Scene: "register",
PhoneHash: phoneHash,
CodeHash: hashIMSecret(tenantID, "register", challengeID, phoneHash, code),
State: types.VerificationStatePending,
MaxAttempts: 5,
ExpiresAt: time.Now().UTC().Add(time.Minute),
}}
cred := &MsgCredClient{Method: "tel", Value: phone, Response: code}
if err := attachLegacyRegistrationSMSChallenge(tenantID, cred, phone, time.Now().UTC()); err != nil {
t.Fatalf("attachLegacyRegistrationSMSChallenge returned %v", err)
}
if cred.Params["challenge_id"] != challengeID {
t.Fatalf("challenge id = %#v, want %q", cred.Params["challenge_id"], challengeID)
}
if cred.Response == code || strings.TrimSpace(cred.Response) == "" {
t.Fatalf("legacy code was not replaced by verification token: %q", cred.Response)
}
verifications := store.Verifications.(*testVerificationStore)
if !verifications.verified {
t.Fatal("verification challenge was not marked verified")
}
wantHash := hashIMSecret(tenantID, "register", challengeID, phoneHash, cred.Response)
if verifications.tokenHash != wantHash {
t.Fatalf("token hash mismatch")
}
}
func TestLegacyFixedRegistrationCredWithoutChallenge(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldUsers := store.Users
oldVerifications := store.Verifications
defer func() {
store.Users = oldUsers
store.Verifications = oldVerifications
}()
tenantID := types.TenantID(7)
phone := "+8618612345678"
store.Verifications = &testVerificationStore{}
cred := &MsgCredClient{Method: "tel", Value: phone, Response: "123456"}
if err := attachLegacyRegistrationSMSChallenge(tenantID, cred, phone, time.Now().UTC()); err != nil {
t.Fatalf("attach legacy fixed code returned %v", err)
}
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
users.EXPECT().UpsertCred(tenantID, gomock.AssignableToTypeOf(&types.Credential{})).
DoAndReturn(func(_ types.TenantID, got *types.Credential) (bool, error) {
if got.User != types.Uid(42).String() || got.Method != "tel" || got.Value != phone || !got.Done {
t.Fatalf("unexpected credential: %#v", got)
}
return true, nil
})
store.Users = users
ok, err := consumeLegacyFixedRegistrationCred(tenantID, 42, cred, func(value string, _ map[string]any) (string, error) {
return "tel:" + value, nil
})
if err != nil || !ok {
t.Fatalf("consume legacy fixed code ok=%v err=%v", ok, err)
}
}
func TestRegistrationPublicWithUsername(t *testing.T) {
if got := registrationPublicWithUsername(nil, "alice").(map[string]any)["fn"]; got != "alice" {
t.Fatalf("got fn=%v", got)
}
public := map[string]any{"fn": "old", "photo": "file1"}
got := registrationPublicWithUsername(public, "bob").(map[string]any)
if got["fn"] != "bob" || got["photo"] != "file1" {
t.Fatalf("got public=%v", got)
}
}
func TestValidateRegistrationInviteStates(t *testing.T) {
oldInvites := store.RegistrationInvites
defer func() { store.RegistrationInvites = oldInvites }()
now := time.Now().UTC()
tests := []struct {
status types.RegistrationInviteStatus
expires time.Time
want error
}{
{types.RegistrationInviteActive, now.Add(time.Minute), nil},
{types.RegistrationInviteActive, now, types.ErrInviteExpired},
{types.RegistrationInviteRevoked, now.Add(time.Minute), types.ErrInviteRevoked},
{types.RegistrationInviteExpired, now.Add(time.Minute), types.ErrInviteExpired},
}
for _, tc := range tests {
store.RegistrationInvites = &testRegistrationInviteStore{invite: &types.RegistrationInviteCode{Status: tc.status, ExpiresAt: tc.expires}}
if got := validateRegistrationInvite(7, "AbCdE", now); got != tc.want {
t.Fatalf("status=%q: got %v want %v", tc.status, got, tc.want)
}
}
}