package main import ( "bytes" "net/http" "net/http/httptest" "strings" "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" ) func TestConsumeProductVerificationCred(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() oldUsers := store.Users oldVerifications := store.Verifications defer func() { store.Users = oldUsers store.Verifications = oldVerifications }() makeTestAPIKey(t) token := "verified-token" tokenHash := hashIMSecret(7, "register", "challenge-1", "phone-hash", token) store.Verifications = &testVerificationStore{get: &types.VerificationCode{ TenantID: 7, ChallengeID: "challenge-1", Scene: "register", PhoneHash: "phone-hash", VerificationTokenHash: &tokenHash, State: types.VerificationStateVerified, ExpiresAt: time.Now().UTC().Add(time.Minute), }} users := mock_store.NewMockUsersPersistenceInterface(ctrl) users.EXPECT().UpsertCred(types.TenantID(7), gomock.AssignableToTypeOf(&types.Credential{})). DoAndReturn(func(_ types.TenantID, cred *types.Credential) (bool, error) { if cred.User != types.Uid(42).String() || cred.Method != "tel" || cred.Value != "+8613800138000" || !cred.Done { t.Fatalf("unexpected credential: %#v", cred) } return true, nil }) store.Users = users value, ok, err := consumeProductVerificationCred(7, 42, &MsgCredClient{ Method: "tel", Value: "+8613800138000", Response: token, Params: map[string]any{"challenge_id": "challenge-1", "countryCode": "CN"}, }, func(value string, _ map[string]any) (string, error) { return "tel:" + value, nil }) if err != nil || !ok || value != "+8613800138000" { t.Fatalf("consumeProductVerificationCred = value %q ok %v err %v", value, ok, err) } if !store.Verifications.(*testVerificationStore).consumed { t.Fatal("expected verification token to be consumed") } } func TestConsumeProductVerificationCredRejectsBadToken(t *testing.T) { oldUsers := store.Users oldVerifications := store.Verifications defer func() { store.Users = oldUsers store.Verifications = oldVerifications }() makeTestAPIKey(t) tokenHash := hashIMSecret(7, "register", "challenge-1", "phone-hash", "good-token") store.Verifications = &testVerificationStore{get: &types.VerificationCode{ TenantID: 7, ChallengeID: "challenge-1", Scene: "register", PhoneHash: "phone-hash", VerificationTokenHash: &tokenHash, State: types.VerificationStateVerified, ExpiresAt: time.Now().UTC().Add(time.Minute), }} _, ok, err := consumeProductVerificationCred(7, 42, &MsgCredClient{ Method: "tel", Value: "+8613800138000", Response: "bad-token", Params: map[string]any{"challenge_id": "challenge-1"}, }, func(value string, _ map[string]any) (string, error) { return "tel:" + value, nil }) if !ok || err != types.ErrInvalidResponse { t.Fatalf("consumeProductVerificationCred bad token ok %v err %v", ok, err) } if store.Verifications.(*testVerificationStore).consumed { t.Fatal("bad token must not be consumed") } } func TestServeIMPasswordResetVerify(t *testing.T) { oldTenants := store.Tenants oldVerifications := store.Verifications defer func() { store.Tenants = oldTenants store.Verifications = oldVerifications }() makeTestAPIKey(t) store.Tenants = testTenantStore{tenant: &types.Tenant{ID: 7, Code: "acme", Name: "Acme", State: types.TenantStateActive}} store.Verifications = &testVerificationStore{get: &types.VerificationCode{ TenantID: 7, ChallengeID: "challenge-1", Scene: "reset_password", PhoneHash: "phone-hash", CodeHash: hashIMSecret(7, "reset_password", "challenge-1", "phone-hash", "123456"), State: types.VerificationStatePending, MaxAttempts: 5, ExpiresAt: time.Now().UTC().Add(time.Minute), }} req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/challenges/challenge-1/verify", bytes.NewBufferString(`{"code":"123456"}`)) req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t)) req.Header.Set("X-Tenant-Code", "acme") rec := httptest.NewRecorder() serveIMPasswordResetVerify(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) } resp := decodeIMHTTPResponse(t, rec) data := resp.Data.(map[string]any) token, _ := data["reset_token"].(string) if !strings.HasPrefix(token, "challenge-1.") { t.Fatalf("unexpected reset token: %#v", data) } } func TestServeIMPasswordResetConfirm(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() oldTenants := store.Tenants oldVerifications := store.Verifications oldUsers := store.Users oldStore := store.Store oldSecurity := store.Security defer func() { store.Tenants = oldTenants store.Verifications = oldVerifications store.Users = oldUsers store.Store = oldStore store.Security = oldSecurity }() makeTestAPIKey(t) token := "reset-secret" tokenHash := hashIMSecret(7, "reset_password", "challenge-1", "phone-hash", token) store.Tenants = testTenantStore{tenant: &types.Tenant{ID: 7, Code: "acme", Name: "Acme", State: types.TenantStateActive}} store.Verifications = &testVerificationStore{get: &types.VerificationCode{ TenantID: 7, ChallengeID: "challenge-1", Scene: "reset_password", PhoneHash: "phone-hash", TargetUser: 42, VerificationTokenHash: &tokenHash, State: types.VerificationStateVerified, ExpiresAt: time.Now().UTC().Add(time.Minute), }} users := mock_store.NewMockUsersPersistenceInterface(ctrl) users.EXPECT().GetAuthRecord(types.TenantID(7), types.Uid(42), "basic"). Return("alice", auth.LevelAuth, []byte("old"), time.Time{}, nil) store.Users = users basicAuth := mock_auth.NewMockAuthHandler(ctrl) basicAuth.EXPECT().IsInitialized().Return(true) basicAuth.EXPECT().UpdateRecord(auth.AuthContext{TenantID: 7}, gomock.Any(), []byte(":new-password")). Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil) mockStore := mock_store.NewMockPersistentStorageInterface(ctrl) mockStore.EXPECT().GetLogicalAuthHandler("basic").Return(basicAuth) store.Store = mockStore security := &testSecurityStore{} store.Security = security req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/confirm", bytes.NewBufferString(`{"reset_token":"challenge-1.`+token+`","new_password":"new-password"}`)) req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t)) req.Header.Set("X-Tenant-Code", "acme") rec := httptest.NewRecorder() serveIMPasswordResetConfirm(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) } if !store.Verifications.(*testVerificationStore).consumed { t.Fatal("expected reset token to be consumed") } if !security.bumped || security.bumpedUser != types.Uid(42) { t.Fatalf("expected auth version bump, got %#v", security) } } type testSecurityStore struct { version int err error bumped bool bumpedUser types.Uid realName *types.RealNameVerification actionToken *types.ActionToken independentPassword *types.IndependentPassword attempts int lockedUntil *time.Time notifications []types.SystemNotification readNotificationID string } func (s *testSecurityStore) GetAuthVersion(types.TenantID, types.Uid) (int, error) { if s.version == 0 { return 1, s.err } return s.version, s.err } func (s *testSecurityStore) BumpAuthVersion(_ types.TenantID, uid types.Uid, _ time.Time) (int, error) { s.bumped = true s.bumpedUser = uid if s.version == 0 { s.version = 1 } s.version++ return s.version, s.err } func (s *testSecurityStore) GetRealName(types.TenantID, types.Uid) (*types.RealNameVerification, error) { if s.err != nil { return nil, s.err } if s.realName == nil { return nil, types.ErrNotFound } return s.realName, nil } func (s *testSecurityStore) CreateRealNameSubmission(_ types.TenantID, rv *types.RealNameVerification) error { s.realName = rv return s.err } func (s *testSecurityStore) CreateActionToken(_ types.TenantID, token *types.ActionToken) error { s.actionToken = token return s.err } func (s *testSecurityStore) ConsumeActionToken(_ types.TenantID, _ types.Uid, _ string, action string, target string, _ time.Time) (*types.ActionToken, error) { if s.err != nil { return nil, s.err } if s.actionToken == nil { return &types.ActionToken{Action: action, Target: target}, nil } return s.actionToken, nil } func (s *testSecurityStore) GetIndependentPassword(types.TenantID, types.Uid, string) (*types.IndependentPassword, error) { if s.err != nil { return nil, s.err } if s.independentPassword == nil { return nil, types.ErrNotFound } return s.independentPassword, nil } func (s *testSecurityStore) UpsertIndependentPassword(_ types.TenantID, pass *types.IndependentPassword) error { s.independentPassword = pass return s.err } func (s *testSecurityStore) UpdateIndependentPasswordAttempts(_ types.TenantID, _ types.Uid, _ string, attempts int, lockedUntil *time.Time, _ time.Time) error { s.attempts = attempts s.lockedUntil = lockedUntil if s.independentPassword != nil { s.independentPassword.FailedAttempts = attempts s.independentPassword.LockedUntil = lockedUntil } return s.err } func (s *testSecurityStore) CreateSystemNotification(_ types.TenantID, notification *types.SystemNotification) error { if notification != nil { s.notifications = append(s.notifications, *notification) } return s.err } func (s *testSecurityStore) ListSystemNotifications(types.TenantID, types.Uid, string, int) ([]types.SystemNotification, string, error) { return s.notifications, "", s.err } func (s *testSecurityStore) MarkSystemNotificationRead(_ types.TenantID, _ types.Uid, eventID string, readAt time.Time) error { s.readNotificationID = eventID for i := range s.notifications { if s.notifications[i].EventID == eventID { s.notifications[i].ReadAt = &readAt return s.err } } if s.err != nil { return s.err } return types.ErrNotFound }