Files
jules/engine/user/user_test.go
T
2026-06-06 18:52:20 +03:00

428 lines
17 KiB
Go

package user
import (
"context"
"errors"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"m8sh.su/d/jules/chat"
"m8sh.su/d/jules/database"
)
type mockDB struct {
users *mockUsers
chats *mockChats
facts *mockFacts
contacts *mockContacts
notifications *mockNotifications
actions *mockActions
}
func (m *mockDB) Users() database.Users { return m.users }
func (m *mockDB) Chats() database.Chats { return m.chats }
func (m *mockDB) Facts() database.Facts { return m.facts }
func (m *mockDB) Contacts() database.Contacts { return m.contacts }
func (m *mockDB) Notifications() database.Notifications { return m.notifications }
func (m *mockDB) Actions() database.Actions { return m.actions }
func (m *mockDB) Close() error { return nil }
type mockUsers struct {
getFunc func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error)
createFunc func(ctx context.Context, user *database.User) error
}
func (m *mockUsers) Get(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return m.getFunc(ctx, id, lookup)
}
func (m *mockUsers) Create(ctx context.Context, user *database.User) error {
return m.createFunc(ctx, user)
}
func (m *mockUsers) Update(ctx context.Context, user *database.User) error { return nil }
func (m *mockUsers) Delete(ctx context.Context, id uuid.UUID) error { return nil }
type mockChats struct {
getUserIDFunc func(ctx context.Context, platform, identifier string) (uuid.UUID, error)
attachFunc func(ctx context.Context, userID uuid.UUID, platform, identifier string) error
listFunc func(ctx context.Context, userID uuid.UUID) ([]database.Chat, error)
}
func (m *mockChats) Attach(ctx context.Context, userID uuid.UUID, platform, identifier string) error {
return m.attachFunc(ctx, userID, platform, identifier)
}
func (m *mockChats) Detach(ctx context.Context, userID uuid.UUID, platform string) error { return nil }
func (m *mockChats) GetUserID(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return m.getUserIDFunc(ctx, platform, identifier)
}
func (m *mockChats) List(ctx context.Context, userID uuid.UUID) ([]database.Chat, error) {
return m.listFunc(ctx, userID)
}
type mockFacts struct {
listFunc func(ctx context.Context, userID uuid.UUID) ([]database.Fact, error)
}
func (m *mockFacts) Add(ctx context.Context, userID uuid.UUID, value string) error { return nil }
func (m *mockFacts) List(ctx context.Context, userID uuid.UUID) ([]database.Fact, error) {
return m.listFunc(ctx, userID)
}
func (m *mockFacts) Delete(ctx context.Context, userID uuid.UUID, value string) error { return nil }
type mockContacts struct {
listFunc func(ctx context.Context, ownerID uuid.UUID) ([]database.Contact, error)
}
func (m *mockContacts) Add(ctx context.Context, contact *database.Contact) error { return nil }
func (m *mockContacts) List(ctx context.Context, ownerID uuid.UUID) ([]database.Contact, error) {
return m.listFunc(ctx, ownerID)
}
func (m *mockContacts) Delete(ctx context.Context, ownerID, targetID uuid.UUID) error { return nil }
type mockNotifications struct {
listFunc func(ctx context.Context, userID uuid.UUID) ([]database.Notification, error)
listOutgoingFunc func(ctx context.Context, initiatorID uuid.UUID) ([]database.Notification, error)
}
func (m *mockNotifications) Push(ctx context.Context, n *database.Notification) error { return nil }
func (m *mockNotifications) Pop(ctx context.Context, limit int) ([]database.Notification, error) {
return nil, nil
}
func (m *mockNotifications) List(ctx context.Context, userID uuid.UUID) ([]database.Notification, error) {
return m.listFunc(ctx, userID)
}
func (m *mockNotifications) ListOutgoing(ctx context.Context, initiatorID uuid.UUID) ([]database.Notification, error) {
return m.listOutgoingFunc(ctx, initiatorID)
}
func (m *mockNotifications) Delete(ctx context.Context, id uuid.UUID) error { return nil }
type mockActions struct {
recentFunc func(ctx context.Context, userID uuid.UUID, limit int) ([]database.Action, error)
}
func (m *mockActions) Log(ctx context.Context, userID uuid.UUID, typ, content string) error {
return nil
}
func (m *mockActions) Recent(ctx context.Context, userID uuid.UUID, limit int) ([]database.Action, error) {
return m.recentFunc(ctx, userID, limit)
}
func TestFromMessage_ExistingUser(t *testing.T) {
userID := uuid.New()
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
db := &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return userID, nil
},
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
return []database.Chat{}, nil
},
},
users: &mockUsers{
getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
},
},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }, listOutgoingFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }},
actions: &mockActions{recentFunc: func(ctx context.Context, id uuid.UUID, limit int) ([]database.Action, error) { return nil, nil }},
}
u, err := FromMessage(t.Context(), db, msg, 10)
require.NoError(t, err)
assert.Equal(t, userID, u.ID)
}
func TestFromMessage_NewUser(t *testing.T) {
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
db := &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return uuid.Nil, database.ErrNotFound
},
attachFunc: func(ctx context.Context, userID uuid.UUID, platform, identifier string) error {
return nil
},
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
return []database.Chat{}, nil
},
},
users: &mockUsers{
createFunc: func(ctx context.Context, user *database.User) error {
return nil
},
},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }, listOutgoingFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }},
actions: &mockActions{recentFunc: func(ctx context.Context, id uuid.UUID, limit int) ([]database.Action, error) { return nil, nil }},
}
u, err := FromMessage(t.Context(), db, msg, 10)
require.NoError(t, err)
assert.NotEqual(t, uuid.Nil, u.ID)
assert.Equal(t, "telegram", u.PreferredChat)
}
func TestFromMessage_ChatsGetUserIDError(t *testing.T) {
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
db := &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return uuid.Nil, errors.New("db down")
},
},
}
_, err := FromMessage(t.Context(), db, msg, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "ensure user by message")
}
func TestFromMessage_UsersGetError(t *testing.T) {
userID := uuid.New()
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
db := &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return userID, nil
},
},
users: &mockUsers{
getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return nil, errors.New("db down")
},
},
}
_, err := FromMessage(t.Context(), db, msg, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "ensure user by message")
}
func TestFromMessage_EnrichmentErrors(t *testing.T) {
userID := uuid.New()
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
tests := []struct {
name string
db *mockDB
errContains string
}{
{
name: "chats list error",
db: &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) { return userID, nil },
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
return nil, errors.New("chats down")
},
},
users: &mockUsers{getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
}},
},
errContains: "list chats",
},
{
name: "facts list error",
db: &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) { return userID, nil },
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) { return nil, nil },
},
users: &mockUsers{getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
}},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) {
return nil, errors.New("facts down")
}},
},
errContains: "list facts",
},
{
name: "contacts list error",
db: &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) { return userID, nil },
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) { return nil, nil },
},
users: &mockUsers{getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
}},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) {
return nil, errors.New("contacts down")
}},
},
errContains: "list contacts",
},
{
name: "incoming notifications error",
db: &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) { return userID, nil },
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) { return nil, nil },
},
users: &mockUsers{getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
}},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) {
return nil, errors.New("notifs down")
},
},
},
errContains: "list incoming notifications",
},
{
name: "outgoing notifications error",
db: &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) { return userID, nil },
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) { return nil, nil },
},
users: &mockUsers{getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
}},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil },
listOutgoingFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) {
return nil, errors.New("outgoing down")
},
},
},
errContains: "list outgoing notifications",
},
{
name: "actions recent error",
db: &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) { return userID, nil },
listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) { return nil, nil },
},
users: &mockUsers{getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: userID}, nil
}},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }, listOutgoingFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }},
actions: &mockActions{recentFunc: func(ctx context.Context, id uuid.UUID, limit int) ([]database.Action, error) {
return nil, errors.New("actions down")
}},
},
errContains: "get recent actions",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := FromMessage(t.Context(), tt.db, msg, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
})
}
}
func TestFromNotification(t *testing.T) {
notif := database.Notification{UserID: uuid.New(), Content: "test"}
db := &mockDB{
users: &mockUsers{
getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return &database.User{ID: id}, nil
},
},
chats: &mockChats{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Chat, error) { return nil, nil }},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }, listOutgoingFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }},
actions: &mockActions{recentFunc: func(ctx context.Context, id uuid.UUID, limit int) ([]database.Action, error) { return nil, nil }},
}
u, err := FromNotification(t.Context(), db, notif, 10)
require.NoError(t, err)
assert.Equal(t, notif.UserID, u.ID)
}
func TestFromNotification_UserNotFound(t *testing.T) {
notif := database.Notification{UserID: uuid.New()}
db := &mockDB{
users: &mockUsers{
getFunc: func(ctx context.Context, id uuid.UUID, lookup database.UserLookup) (*database.User, error) {
return nil, database.ErrNotFound
},
},
}
_, err := FromNotification(t.Context(), db, notif, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "get user by id")
}
func TestCreateUserAndAttachChat_CreateError(t *testing.T) {
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
db := &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return uuid.Nil, database.ErrNotFound
},
},
users: &mockUsers{
createFunc: func(ctx context.Context, user *database.User) error {
return errors.New("create failed")
},
},
}
_, err := FromMessage(t.Context(), db, msg, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "create user")
}
func TestCreateUserAndAttachChat_AttachError(t *testing.T) {
msg := chat.Message{Chat: "telegram", ID: "123", Text: "hello"}
db := &mockDB{
chats: &mockChats{
getUserIDFunc: func(ctx context.Context, platform, identifier string) (uuid.UUID, error) {
return uuid.Nil, database.ErrNotFound
},
attachFunc: func(ctx context.Context, userID uuid.UUID, platform, identifier string) error {
return errors.New("attach failed")
},
},
users: &mockUsers{
createFunc: func(ctx context.Context, user *database.User) error {
return nil
},
},
facts: &mockFacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Fact, error) { return nil, nil }},
contacts: &mockContacts{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Contact, error) { return nil, nil }},
notifications: &mockNotifications{listFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }, listOutgoingFunc: func(ctx context.Context, id uuid.UUID) ([]database.Notification, error) { return nil, nil }},
actions: &mockActions{recentFunc: func(ctx context.Context, id uuid.UUID, limit int) ([]database.Action, error) { return nil, nil }},
}
_, err := FromMessage(t.Context(), db, msg, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "attach chat")
}