Skip to content

Commit 687c343

Browse files
committed
Merge branch 'main' of https://github.com/lobehub/lobe-chat
2 parents f86e21d + 59bdf12 commit 687c343

File tree

5 files changed

+280
-1
lines changed

5 files changed

+280
-1
lines changed

.github/ISSUE_TEMPLATE/1_bug_report_cn.yml

+8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ name: '🐛 反馈缺陷'
22
description: '反馈一个问题缺陷'
33
title: '[Bug] '
44
labels: ['🐛 Bug']
5+
type: Bug
56
body:
7+
- type: markdown
8+
attributes:
9+
value: |
10+
在创建新的 Issue 之前,请先[搜索已有问题](https://github.com/lobehub/lobe-chat/issues),如果发现已有类似的问题,请给它 **👍 点赞**,这样可以帮助我们更快地解决问题。
11+
如果你在使用过程中遇到问题,可以尝试以下方式获取帮助:
12+
- 在 [GitHub Discussions](https://github.com/lobehub/lobe-chat/discussions) 的版块发起讨论。
13+
- 在 [LobeChat 社区](https://discord.gg/AYFPHvv2jT) 提问,与其他用户交流。
614
- type: dropdown
715
attributes:
816
label: '📦 部署环境'

.github/ISSUE_TEMPLATE/config.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
contact_links:
2-
- name: Questions and ideas | 问题和想法
2+
- name: Ask a question for self-hosting | 咨询自部署问题
3+
url: https://github.com/lobehub/lobe-chat/discussions/new?category=self-hosting-%E7%A7%81%E6%9C%89%E5%8C%96%E9%83%A8%E7%BD%B2
4+
about: Please post questions, and ideas in discussions. | 请在讨论区发布问题和想法。
5+
- name: Questions and ideas | 其他问题和想法
36
url: https://github.com/lobehub/lobe-chat/discussions/new/choose
47
about: Please post questions, and ideas in discussions. | 请在讨论区发布问题和想法。
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// @vitest-environment node
2+
import { NextResponse } from 'next/server';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import { UserItem } from '@/database/schemas';
6+
import { serverDB } from '@/database/server';
7+
import { UserModel } from '@/database/server/models/user';
8+
import { pino } from '@/libs/logger';
9+
import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
10+
11+
import { NextAuthUserService } from './index';
12+
13+
vi.mock('@/libs/logger', () => ({
14+
pino: {
15+
info: vi.fn(),
16+
warn: vi.fn(),
17+
},
18+
}));
19+
20+
vi.mock('@/database/server/models/user');
21+
vi.mock('@/database/server');
22+
23+
describe('NextAuthUserService', () => {
24+
let service: NextAuthUserService;
25+
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
service = new NextAuthUserService();
29+
});
30+
31+
describe('safeUpdateUser', () => {
32+
const mockUser = {
33+
id: 'user-123',
34+
35+
};
36+
37+
const mockAccount = {
38+
provider: 'github',
39+
providerAccountId: '12345',
40+
};
41+
42+
const mockUpdateData: Partial<UserItem> = {
43+
avatar: 'https://example.com/avatar.jpg',
44+
45+
fullName: 'Test User',
46+
};
47+
48+
it('should update user when user is found', async () => {
49+
const mockUserModel = {
50+
updateUser: vi.fn().mockResolvedValue({}),
51+
};
52+
53+
vi.mocked(UserModel).mockImplementation(() => mockUserModel as any);
54+
55+
// Mock the adapter directly on the service instance
56+
service.adapter = {
57+
getUserByAccount: vi.fn().mockResolvedValue(mockUser),
58+
};
59+
60+
const response = await service.safeUpdateUser(mockAccount, mockUpdateData);
61+
62+
expect(pino.info).toHaveBeenCalledWith(
63+
`updating user "${JSON.stringify(mockAccount)}" due to webhook`,
64+
);
65+
66+
expect(service.adapter.getUserByAccount).toHaveBeenCalledWith(mockAccount);
67+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUser.id);
68+
expect(mockUserModel.updateUser).toHaveBeenCalledWith(mockUpdateData);
69+
70+
expect(response).toBeInstanceOf(NextResponse);
71+
expect(response.status).toBe(200);
72+
const data = await response.json();
73+
expect(data).toEqual({ message: 'user updated', success: true });
74+
});
75+
76+
it('should handle case when user is not found', async () => {
77+
// Mock the adapter directly on the service instance
78+
service.adapter = {
79+
getUserByAccount: vi.fn().mockResolvedValue(null),
80+
};
81+
82+
const response = await service.safeUpdateUser(mockAccount, mockUpdateData);
83+
84+
expect(pino.warn).toHaveBeenCalledWith(
85+
`[${mockAccount.provider}]: Webhooks handler user "${JSON.stringify(mockAccount)}" update for "${JSON.stringify(mockUpdateData)}", but no user was found by the providerAccountId.`,
86+
);
87+
88+
expect(UserModel).not.toHaveBeenCalled();
89+
90+
expect(response).toBeInstanceOf(NextResponse);
91+
expect(response.status).toBe(200);
92+
const data = await response.json();
93+
expect(data).toEqual({ message: 'user updated', success: true });
94+
});
95+
96+
it('should handle errors during user update', async () => {
97+
const mockError = new Error('Database error');
98+
99+
// Mock the adapter directly on the service instance
100+
service.adapter = {
101+
getUserByAccount: vi.fn().mockRejectedValue(mockError),
102+
};
103+
104+
await expect(service.safeUpdateUser(mockAccount, mockUpdateData)).rejects.toThrow(mockError);
105+
106+
expect(UserModel).not.toHaveBeenCalled();
107+
});
108+
});
109+
});

src/services/user/client.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,14 @@ describe('ClientService', () => {
9595
expect(spyOn).toHaveBeenCalledWith(newPreference);
9696
expect(spyOn).toHaveBeenCalledTimes(1);
9797
});
98+
99+
it('should return empty array for getUserSSOProviders', async () => {
100+
const providers = await clientService.getUserSSOProviders();
101+
expect(providers).toEqual([]);
102+
});
103+
104+
it('should do nothing when unlinkSSOProvider is called', async () => {
105+
const result = await clientService.unlinkSSOProvider('google', '123');
106+
expect(result).toBeUndefined();
107+
});
98108
});

src/services/user/server.test.ts

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { DeepPartial } from 'utility-types';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { lambdaClient } from '@/libs/trpc/client';
5+
import { UserInitializationState, UserPreference } from '@/types/user';
6+
import { UserSettings } from '@/types/user/settings';
7+
8+
import { ServerService } from './server';
9+
10+
vi.mock('@/libs/trpc/client', () => ({
11+
lambdaClient: {
12+
user: {
13+
getUserRegistrationDuration: {
14+
query: vi.fn(),
15+
},
16+
getUserState: {
17+
query: vi.fn(),
18+
},
19+
getUserSSOProviders: {
20+
query: vi.fn(),
21+
},
22+
unlinkSSOProvider: {
23+
mutate: vi.fn(),
24+
},
25+
makeUserOnboarded: {
26+
mutate: vi.fn(),
27+
},
28+
updatePreference: {
29+
mutate: vi.fn(),
30+
},
31+
updateGuide: {
32+
mutate: vi.fn(),
33+
},
34+
updateSettings: {
35+
mutate: vi.fn(),
36+
},
37+
resetSettings: {
38+
mutate: vi.fn(),
39+
},
40+
},
41+
},
42+
}));
43+
44+
describe('ServerService', () => {
45+
const service = new ServerService();
46+
47+
it('should get user registration duration', async () => {
48+
const mockData = {
49+
createdAt: '2023-01-01',
50+
duration: 100,
51+
updatedAt: '2023-01-02',
52+
};
53+
vi.mocked(lambdaClient.user.getUserRegistrationDuration.query).mockResolvedValue(mockData);
54+
55+
const result = await service.getUserRegistrationDuration();
56+
expect(result).toEqual(mockData);
57+
});
58+
59+
it('should get user state', async () => {
60+
const mockState: UserInitializationState = {
61+
isOnboard: true,
62+
preference: {
63+
telemetry: true,
64+
},
65+
settings: {},
66+
};
67+
vi.mocked(lambdaClient.user.getUserState.query).mockResolvedValue(mockState);
68+
69+
const result = await service.getUserState();
70+
expect(result).toEqual(mockState);
71+
});
72+
73+
it('should get user SSO providers', async () => {
74+
const mockProviders = [
75+
{
76+
provider: 'google',
77+
providerAccountId: '123',
78+
userId: 'user1',
79+
type: 'oauth' as const,
80+
access_token: 'token',
81+
token_type: 'bearer' as const,
82+
expires_at: 123,
83+
scope: 'email profile',
84+
},
85+
];
86+
vi.mocked(lambdaClient.user.getUserSSOProviders.query).mockResolvedValue(mockProviders);
87+
88+
const result = await service.getUserSSOProviders();
89+
expect(result).toEqual(mockProviders);
90+
});
91+
92+
it('should unlink SSO provider', async () => {
93+
const provider = 'google';
94+
const providerAccountId = '123';
95+
await service.unlinkSSOProvider(provider, providerAccountId);
96+
97+
expect(lambdaClient.user.unlinkSSOProvider.mutate).toHaveBeenCalledWith({
98+
provider,
99+
providerAccountId,
100+
});
101+
});
102+
103+
it('should make user onboarded', async () => {
104+
await service.makeUserOnboarded();
105+
expect(lambdaClient.user.makeUserOnboarded.mutate).toHaveBeenCalled();
106+
});
107+
108+
it('should update user preference', async () => {
109+
const preference: Partial<UserPreference> = {
110+
telemetry: true,
111+
useCmdEnterToSend: true,
112+
};
113+
await service.updatePreference(preference);
114+
expect(lambdaClient.user.updatePreference.mutate).toHaveBeenCalledWith(preference);
115+
});
116+
117+
it('should update user guide', async () => {
118+
const guide = {
119+
moveSettingsToAvatar: true,
120+
topic: false,
121+
uploadFileInKnowledgeBase: true,
122+
};
123+
await service.updateGuide(guide);
124+
expect(lambdaClient.user.updateGuide.mutate).toHaveBeenCalledWith(guide);
125+
});
126+
127+
it('should update user settings', async () => {
128+
const settings: DeepPartial<UserSettings> = {
129+
defaultAgent: {
130+
config: {
131+
model: 'gpt-4',
132+
provider: 'openai',
133+
},
134+
meta: {
135+
avatar: 'avatar',
136+
description: 'test agent',
137+
},
138+
},
139+
};
140+
const signal = new AbortController().signal;
141+
await service.updateUserSettings(settings, signal);
142+
expect(lambdaClient.user.updateSettings.mutate).toHaveBeenCalledWith(settings, { signal });
143+
});
144+
145+
it('should reset user settings', async () => {
146+
await service.resetUserSettings();
147+
expect(lambdaClient.user.resetSettings.mutate).toHaveBeenCalled();
148+
});
149+
});

0 commit comments

Comments
 (0)