@lobehub/chat 1.61.5 → 1.61.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/self-hosting/advanced/auth/next-auth/casdoor.mdx +2 -1
- package/docs/self-hosting/advanced/auth/next-auth/casdoor.zh-CN.mdx +2 -1
- package/package.json +1 -1
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -7
- package/src/app/(backend)/api/webhooks/casdoor/validateRequest.ts +7 -4
- package/src/server/globalConfig/index.test.ts +81 -0
- package/src/server/routers/lambda/user.test.ts +305 -0
- package/src/server/services/nextAuthUser/index.ts +2 -2
- package/src/utils/errorResponse.test.ts +37 -1
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.61.6](https://github.com/lobehub/lobe-chat/compare/v1.61.5...v1.61.6)
|
6
|
+
|
7
|
+
<sup>Released on **2025-02-20**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Casdoor webhooks error.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's fixed
|
19
|
+
|
20
|
+
- **misc**: Casdoor webhooks error, closes [#6304](https://github.com/lobehub/lobe-chat/issues/6304) ([7a458b9](https://github.com/lobehub/lobe-chat/commit/7a458b9))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.61.5](https://github.com/lobehub/lobe-chat/compare/v1.61.4...v1.61.5)
|
6
31
|
|
7
32
|
<sup>Released on **2025-02-19**</sup>
|
package/changelog/v1.json
CHANGED
@@ -106,6 +106,8 @@ If you are deploying using a public network, the following assumptions apply:
|
|
106
106
|
|
107
107
|
### Configure Webhook (Optional)
|
108
108
|
|
109
|
+
> Available on Casdoor `>=1.843.0`.
|
110
|
+
|
109
111
|
Configure the Casdoor webhook so that LobeChat can receive notifications when user information is updated.
|
110
112
|
|
111
113
|
Go to `Admin` -> `Webhooks`, add a webhook, and fill in the following fields:
|
@@ -118,7 +120,6 @@ If you are deploying using a public network, the following assumptions apply:
|
|
118
120
|
> The secret is generated by yourself, you can visit [https://generate-secret.vercel.app/10](https://generate-secret.vercel.app/10) to generate a 10 bit secret.
|
119
121
|
|
120
122
|
- Event: `update-user`
|
121
|
-
- Is user extented: `true`
|
122
123
|
|
123
124
|
Save and Exit, then copy the Webhook secret and fill it in the environment variable \`CASDOOR\_WEBHOOK\_SECRET.
|
124
125
|
|
@@ -95,6 +95,8 @@ tags:
|
|
95
95
|
|
96
96
|
### 配置 Webhook (可选)
|
97
97
|
|
98
|
+
> 在 Casdoor `>=1.843.0` 上可用。
|
99
|
+
|
98
100
|
配置 Casdoor 的 Webhook 以便在用户信息更新时同步到 LobeChat 。
|
99
101
|
|
100
102
|
前往 `管理工具` -> `Webhooks`,创建一个 Webhook,添加一个 Webhook,填写以下字段:
|
@@ -107,7 +109,6 @@ tags:
|
|
107
109
|
> 密钥由你自己生成,用于验证 Casdoor 发送的请求是否合法。 可以前往 [https://generate-secret.vercel.app/10](https://generate-secret.vercel.app/10) 生成一个 10 位的密钥。
|
108
110
|
|
109
111
|
- 事件:`update-user`
|
110
|
-
- 拓展用户字段:`true`
|
111
112
|
|
112
113
|
保存,并退出。 将该密钥填写到环境变量中的 `CASDOOR_WEBHOOK_SECRET`。
|
113
114
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.61.
|
3
|
+
"version": "1.61.6",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -16,9 +16,7 @@ export const POST = async (req: Request): Promise<NextResponse> => {
|
|
16
16
|
);
|
17
17
|
}
|
18
18
|
|
19
|
-
const { action,
|
20
|
-
|
21
|
-
pino.trace(`casdoor webhook payload: ${{ action, extendedUser }}`);
|
19
|
+
const { action, object } = payload;
|
22
20
|
|
23
21
|
const nextAuthUserService = new NextAuthUserService();
|
24
22
|
switch (action) {
|
@@ -26,12 +24,12 @@ export const POST = async (req: Request): Promise<NextResponse> => {
|
|
26
24
|
return nextAuthUserService.safeUpdateUser(
|
27
25
|
{
|
28
26
|
provider: 'casdoor',
|
29
|
-
providerAccountId:
|
27
|
+
providerAccountId: object.id,
|
30
28
|
},
|
31
29
|
{
|
32
|
-
avatar:
|
33
|
-
email:
|
34
|
-
fullName:
|
30
|
+
avatar: object?.avatar,
|
31
|
+
email: object?.email,
|
32
|
+
fullName: object.displayName,
|
35
33
|
},
|
36
34
|
);
|
37
35
|
}
|
@@ -11,8 +11,9 @@ export type CasdoorUserEntity = {
|
|
11
11
|
|
12
12
|
interface CasdoorWebhookPayload {
|
13
13
|
action: string;
|
14
|
-
//
|
15
|
-
|
14
|
+
// The object is the user entity that is updated.
|
15
|
+
// ref: https://github.com/casdoor/casdoor/issues/1918#issuecomment-1572218847
|
16
|
+
object: CasdoorUserEntity;
|
16
17
|
}
|
17
18
|
|
18
19
|
export const validateRequest = async (request: Request, secret?: string) => {
|
@@ -21,7 +22,9 @@ export const validateRequest = async (request: Request, secret?: string) => {
|
|
21
22
|
const casdoorSecret = headerPayload.get('casdoor-secret')!;
|
22
23
|
try {
|
23
24
|
if (casdoorSecret === secret) {
|
24
|
-
return JSON.parse(payloadString)
|
25
|
+
return JSON.parse(payloadString, (k, v) =>
|
26
|
+
k === 'object' && typeof v === 'string' ? JSON.parse(v) : v,
|
27
|
+
) as CasdoorWebhookPayload;
|
25
28
|
} else {
|
26
29
|
console.warn(
|
27
30
|
'[Casdoor]: secret verify failed, please check your secret in `CASDOOR_WEBHOOK_SECRET`',
|
@@ -32,7 +35,7 @@ export const validateRequest = async (request: Request, secret?: string) => {
|
|
32
35
|
if (!authEnv.CASDOOR_WEBHOOK_SECRET) {
|
33
36
|
throw new Error('`CASDOOR_WEBHOOK_SECRET` environment variable is missing.');
|
34
37
|
}
|
35
|
-
console.error('[Casdoor]: incoming webhook failed in verification.\n', e);
|
38
|
+
console.error('[Casdoor]: incoming webhook failed in verification.\n', e, payloadString);
|
36
39
|
return;
|
37
40
|
}
|
38
41
|
};
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { getAppConfig } from '@/config/app';
|
4
|
+
import { knowledgeEnv } from '@/config/knowledge';
|
5
|
+
import { SystemEmbeddingConfig } from '@/types/knowledgeBase';
|
6
|
+
import { FilesConfigItem } from '@/types/user/settings/filesConfig';
|
7
|
+
|
8
|
+
import { getServerDefaultAgentConfig, getServerDefaultFilesConfig } from './index';
|
9
|
+
import { parseAgentConfig } from './parseDefaultAgent';
|
10
|
+
import { parseFilesConfig } from './parseFilesConfig';
|
11
|
+
|
12
|
+
vi.mock('@/config/app', () => ({
|
13
|
+
getAppConfig: vi.fn(),
|
14
|
+
}));
|
15
|
+
|
16
|
+
vi.mock('@/config/knowledge', () => ({
|
17
|
+
knowledgeEnv: {
|
18
|
+
DEFAULT_FILES_CONFIG: 'test_config',
|
19
|
+
},
|
20
|
+
}));
|
21
|
+
|
22
|
+
vi.mock('./parseDefaultAgent', () => ({
|
23
|
+
parseAgentConfig: vi.fn(),
|
24
|
+
}));
|
25
|
+
|
26
|
+
vi.mock('./parseFilesConfig', () => ({
|
27
|
+
parseFilesConfig: vi.fn(),
|
28
|
+
}));
|
29
|
+
|
30
|
+
describe('getServerDefaultAgentConfig', () => {
|
31
|
+
it('should return parsed agent config', () => {
|
32
|
+
const mockConfig = { key: 'value' };
|
33
|
+
vi.mocked(getAppConfig).mockReturnValue({
|
34
|
+
DEFAULT_AGENT_CONFIG: 'test_agent_config',
|
35
|
+
} as any);
|
36
|
+
vi.mocked(parseAgentConfig).mockReturnValue(mockConfig);
|
37
|
+
|
38
|
+
const result = getServerDefaultAgentConfig();
|
39
|
+
|
40
|
+
expect(parseAgentConfig).toHaveBeenCalledWith('test_agent_config');
|
41
|
+
expect(result).toEqual(mockConfig);
|
42
|
+
});
|
43
|
+
|
44
|
+
it('should return empty object if parseAgentConfig returns undefined', () => {
|
45
|
+
vi.mocked(getAppConfig).mockReturnValue({
|
46
|
+
DEFAULT_AGENT_CONFIG: 'test_agent_config',
|
47
|
+
} as any);
|
48
|
+
vi.mocked(parseAgentConfig).mockReturnValue(undefined);
|
49
|
+
|
50
|
+
const result = getServerDefaultAgentConfig();
|
51
|
+
|
52
|
+
expect(result).toEqual({});
|
53
|
+
});
|
54
|
+
});
|
55
|
+
|
56
|
+
describe('getServerDefaultFilesConfig', () => {
|
57
|
+
it('should return parsed files config', () => {
|
58
|
+
const mockEmbeddingModel: FilesConfigItem = {
|
59
|
+
model: 'test-model',
|
60
|
+
provider: 'test-provider',
|
61
|
+
};
|
62
|
+
|
63
|
+
const mockRerankerModel: FilesConfigItem = {
|
64
|
+
model: 'test-reranker',
|
65
|
+
provider: 'test-provider',
|
66
|
+
};
|
67
|
+
|
68
|
+
const mockConfig: SystemEmbeddingConfig = {
|
69
|
+
embeddingModel: mockEmbeddingModel,
|
70
|
+
queryMode: 'hybrid',
|
71
|
+
rerankerModel: mockRerankerModel,
|
72
|
+
};
|
73
|
+
|
74
|
+
vi.mocked(parseFilesConfig).mockReturnValue(mockConfig);
|
75
|
+
|
76
|
+
const result = getServerDefaultFilesConfig();
|
77
|
+
|
78
|
+
expect(parseFilesConfig).toHaveBeenCalledWith('test_config');
|
79
|
+
expect(result).toEqual(mockConfig);
|
80
|
+
});
|
81
|
+
});
|
@@ -0,0 +1,305 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { enableClerk } from '@/const/auth';
|
5
|
+
import { serverDB } from '@/database/server';
|
6
|
+
import { MessageModel } from '@/database/server/models/message';
|
7
|
+
import { SessionModel } from '@/database/server/models/session';
|
8
|
+
import { UserModel, UserNotFoundError } from '@/database/server/models/user';
|
9
|
+
import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
|
10
|
+
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
11
|
+
import { UserService } from '@/server/services/user';
|
12
|
+
|
13
|
+
import { userRouter } from './user';
|
14
|
+
|
15
|
+
// Mock modules
|
16
|
+
vi.mock('@clerk/nextjs/server', () => ({
|
17
|
+
currentUser: vi.fn(),
|
18
|
+
}));
|
19
|
+
|
20
|
+
vi.mock('@/database/server', () => ({
|
21
|
+
serverDB: {},
|
22
|
+
}));
|
23
|
+
|
24
|
+
vi.mock('@/database/server/models/message');
|
25
|
+
vi.mock('@/database/server/models/session');
|
26
|
+
vi.mock('@/database/server/models/user');
|
27
|
+
vi.mock('@/libs/next-auth/adapter');
|
28
|
+
vi.mock('@/server/modules/KeyVaultsEncrypt');
|
29
|
+
vi.mock('@/server/services/user');
|
30
|
+
vi.mock('@/const/auth', () => ({
|
31
|
+
enableClerk: true,
|
32
|
+
}));
|
33
|
+
|
34
|
+
describe('userRouter', () => {
|
35
|
+
const mockUserId = 'test-user-id';
|
36
|
+
const mockCtx = {
|
37
|
+
userId: mockUserId,
|
38
|
+
};
|
39
|
+
|
40
|
+
beforeEach(() => {
|
41
|
+
vi.clearAllMocks();
|
42
|
+
});
|
43
|
+
|
44
|
+
describe('getUserRegistrationDuration', () => {
|
45
|
+
it('should return registration duration', async () => {
|
46
|
+
const mockDuration = { duration: 100, createdAt: '2023-01-01', updatedAt: '2023-01-02' };
|
47
|
+
vi.mocked(UserModel).mockImplementation(
|
48
|
+
() =>
|
49
|
+
({
|
50
|
+
getUserRegistrationDuration: vi.fn().mockResolvedValue(mockDuration),
|
51
|
+
}) as any,
|
52
|
+
);
|
53
|
+
|
54
|
+
const result = await userRouter.createCaller({ ...mockCtx }).getUserRegistrationDuration();
|
55
|
+
|
56
|
+
expect(result).toEqual(mockDuration);
|
57
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
58
|
+
});
|
59
|
+
});
|
60
|
+
|
61
|
+
describe('getUserSSOProviders', () => {
|
62
|
+
it('should return SSO providers', async () => {
|
63
|
+
const mockProviders = [
|
64
|
+
{
|
65
|
+
provider: 'google',
|
66
|
+
providerAccountId: '123',
|
67
|
+
userId: 'user-1',
|
68
|
+
type: 'oauth',
|
69
|
+
},
|
70
|
+
];
|
71
|
+
vi.mocked(UserModel).mockImplementation(
|
72
|
+
() =>
|
73
|
+
({
|
74
|
+
getUserSSOProviders: vi.fn().mockResolvedValue(mockProviders),
|
75
|
+
}) as any,
|
76
|
+
);
|
77
|
+
|
78
|
+
const result = await userRouter.createCaller({ ...mockCtx }).getUserSSOProviders();
|
79
|
+
|
80
|
+
expect(result).toEqual(mockProviders);
|
81
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
82
|
+
});
|
83
|
+
});
|
84
|
+
|
85
|
+
describe('getUserState', () => {
|
86
|
+
it('should return user state', async () => {
|
87
|
+
const mockState = {
|
88
|
+
isOnboarded: true,
|
89
|
+
preference: { telemetry: true },
|
90
|
+
settings: {},
|
91
|
+
userId: mockUserId,
|
92
|
+
};
|
93
|
+
|
94
|
+
vi.mocked(UserModel).mockImplementation(
|
95
|
+
() =>
|
96
|
+
({
|
97
|
+
getUserState: vi.fn().mockResolvedValue(mockState),
|
98
|
+
}) as any,
|
99
|
+
);
|
100
|
+
|
101
|
+
vi.mocked(MessageModel).mockImplementation(
|
102
|
+
() =>
|
103
|
+
({
|
104
|
+
hasMoreThanN: vi.fn().mockResolvedValue(true),
|
105
|
+
}) as any,
|
106
|
+
);
|
107
|
+
|
108
|
+
vi.mocked(SessionModel).mockImplementation(
|
109
|
+
() =>
|
110
|
+
({
|
111
|
+
hasMoreThanN: vi.fn().mockResolvedValue(true),
|
112
|
+
}) as any,
|
113
|
+
);
|
114
|
+
|
115
|
+
const result = await userRouter.createCaller({ ...mockCtx }).getUserState();
|
116
|
+
|
117
|
+
expect(result).toEqual({
|
118
|
+
isOnboard: true,
|
119
|
+
preference: { telemetry: true },
|
120
|
+
settings: {},
|
121
|
+
hasConversation: true,
|
122
|
+
canEnablePWAGuide: true,
|
123
|
+
canEnableTrace: true,
|
124
|
+
userId: mockUserId,
|
125
|
+
});
|
126
|
+
});
|
127
|
+
|
128
|
+
it('should create new user when user not found (clerk enabled)', async () => {
|
129
|
+
const mockClerkUser = {
|
130
|
+
id: mockUserId,
|
131
|
+
createdAt: new Date(),
|
132
|
+
emailAddresses: [{ id: 'email-1', emailAddress: 'test@example.com' }],
|
133
|
+
firstName: 'Test',
|
134
|
+
lastName: 'User',
|
135
|
+
imageUrl: 'avatar.jpg',
|
136
|
+
phoneNumbers: [],
|
137
|
+
primaryEmailAddressId: 'email-1',
|
138
|
+
primaryPhoneNumberId: null,
|
139
|
+
username: 'testuser',
|
140
|
+
};
|
141
|
+
|
142
|
+
const { currentUser } = await import('@clerk/nextjs/server');
|
143
|
+
vi.mocked(currentUser).mockResolvedValue(mockClerkUser as any);
|
144
|
+
|
145
|
+
vi.mocked(UserService).mockImplementation(
|
146
|
+
() =>
|
147
|
+
({
|
148
|
+
createUser: vi.fn().mockResolvedValue({ success: true }),
|
149
|
+
}) as any,
|
150
|
+
);
|
151
|
+
|
152
|
+
vi.mocked(UserModel).mockImplementation(
|
153
|
+
() =>
|
154
|
+
({
|
155
|
+
getUserState: vi
|
156
|
+
.fn()
|
157
|
+
.mockRejectedValueOnce(new UserNotFoundError())
|
158
|
+
.mockResolvedValueOnce({
|
159
|
+
isOnboarded: false,
|
160
|
+
preference: { telemetry: null },
|
161
|
+
settings: {},
|
162
|
+
}),
|
163
|
+
}) as any,
|
164
|
+
);
|
165
|
+
|
166
|
+
vi.mocked(MessageModel).mockImplementation(
|
167
|
+
() =>
|
168
|
+
({
|
169
|
+
hasMoreThanN: vi.fn().mockResolvedValue(false),
|
170
|
+
}) as any,
|
171
|
+
);
|
172
|
+
|
173
|
+
vi.mocked(SessionModel).mockImplementation(
|
174
|
+
() =>
|
175
|
+
({
|
176
|
+
hasMoreThanN: vi.fn().mockResolvedValue(false),
|
177
|
+
}) as any,
|
178
|
+
);
|
179
|
+
|
180
|
+
const result = await userRouter.createCaller({ ...mockCtx } as any).getUserState();
|
181
|
+
|
182
|
+
expect(result).toEqual({
|
183
|
+
isOnboard: true,
|
184
|
+
preference: { telemetry: null },
|
185
|
+
settings: {},
|
186
|
+
hasConversation: false,
|
187
|
+
canEnablePWAGuide: false,
|
188
|
+
canEnableTrace: false,
|
189
|
+
userId: mockUserId,
|
190
|
+
});
|
191
|
+
});
|
192
|
+
});
|
193
|
+
|
194
|
+
describe('makeUserOnboarded', () => {
|
195
|
+
it('should update user onboarded status', async () => {
|
196
|
+
vi.mocked(UserModel).mockImplementation(
|
197
|
+
() =>
|
198
|
+
({
|
199
|
+
updateUser: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
200
|
+
}) as any,
|
201
|
+
);
|
202
|
+
|
203
|
+
await userRouter.createCaller({ ...mockCtx }).makeUserOnboarded();
|
204
|
+
|
205
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
206
|
+
});
|
207
|
+
});
|
208
|
+
|
209
|
+
describe('unlinkSSOProvider', () => {
|
210
|
+
it('should unlink SSO provider successfully', async () => {
|
211
|
+
const mockInput = {
|
212
|
+
provider: 'google',
|
213
|
+
providerAccountId: '123',
|
214
|
+
};
|
215
|
+
|
216
|
+
const mockAccount = {
|
217
|
+
userId: mockUserId,
|
218
|
+
provider: 'google',
|
219
|
+
providerAccountId: '123',
|
220
|
+
type: 'oauth',
|
221
|
+
};
|
222
|
+
|
223
|
+
vi.mocked(LobeNextAuthDbAdapter).mockReturnValue({
|
224
|
+
getAccount: vi.fn().mockResolvedValue(mockAccount),
|
225
|
+
unlinkAccount: vi.fn().mockResolvedValue(undefined),
|
226
|
+
} as any);
|
227
|
+
|
228
|
+
await expect(
|
229
|
+
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
230
|
+
).resolves.not.toThrow();
|
231
|
+
});
|
232
|
+
|
233
|
+
it('should throw error if account does not exist', async () => {
|
234
|
+
const mockInput = {
|
235
|
+
provider: 'google',
|
236
|
+
providerAccountId: '123',
|
237
|
+
};
|
238
|
+
|
239
|
+
vi.mocked(LobeNextAuthDbAdapter).mockReturnValue({
|
240
|
+
getAccount: vi.fn().mockResolvedValue(null),
|
241
|
+
unlinkAccount: vi.fn(),
|
242
|
+
} as any);
|
243
|
+
|
244
|
+
await expect(
|
245
|
+
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
246
|
+
).rejects.toThrow('The account does not exist');
|
247
|
+
});
|
248
|
+
|
249
|
+
it('should throw error if adapter methods are not implemented', async () => {
|
250
|
+
const mockInput = {
|
251
|
+
provider: 'google',
|
252
|
+
providerAccountId: '123',
|
253
|
+
};
|
254
|
+
|
255
|
+
vi.mocked(LobeNextAuthDbAdapter).mockReturnValue({} as any);
|
256
|
+
|
257
|
+
await expect(
|
258
|
+
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
259
|
+
).rejects.toThrow('The method in LobeNextAuthDbAdapter `unlinkAccount` is not implemented');
|
260
|
+
});
|
261
|
+
});
|
262
|
+
|
263
|
+
describe('updateSettings', () => {
|
264
|
+
it('should update settings with encrypted key vaults', async () => {
|
265
|
+
const mockSettings = {
|
266
|
+
keyVaults: { openai: { key: 'test-key' } },
|
267
|
+
general: { language: 'en-US' },
|
268
|
+
};
|
269
|
+
|
270
|
+
const mockEncryptedVaults = 'encrypted-data';
|
271
|
+
const mockGateKeeper = {
|
272
|
+
encrypt: vi.fn().mockResolvedValue(mockEncryptedVaults),
|
273
|
+
};
|
274
|
+
|
275
|
+
vi.mocked(KeyVaultsGateKeeper.initWithEnvKey).mockResolvedValue(mockGateKeeper as any);
|
276
|
+
vi.mocked(UserModel).mockImplementation(
|
277
|
+
() =>
|
278
|
+
({
|
279
|
+
updateSetting: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
280
|
+
}) as any,
|
281
|
+
);
|
282
|
+
|
283
|
+
await userRouter.createCaller({ ...mockCtx }).updateSettings(mockSettings);
|
284
|
+
|
285
|
+
expect(mockGateKeeper.encrypt).toHaveBeenCalledWith(JSON.stringify(mockSettings.keyVaults));
|
286
|
+
});
|
287
|
+
|
288
|
+
it('should update settings without key vaults', async () => {
|
289
|
+
const mockSettings = {
|
290
|
+
general: { language: 'en-US' },
|
291
|
+
};
|
292
|
+
|
293
|
+
vi.mocked(UserModel).mockImplementation(
|
294
|
+
() =>
|
295
|
+
({
|
296
|
+
updateSetting: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
297
|
+
}) as any,
|
298
|
+
);
|
299
|
+
|
300
|
+
await userRouter.createCaller({ ...mockCtx }).updateSettings(mockSettings);
|
301
|
+
|
302
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
303
|
+
});
|
304
|
+
});
|
305
|
+
});
|
@@ -17,7 +17,7 @@ export class NextAuthUserService {
|
|
17
17
|
{ providerAccountId, provider }: { provider: string; providerAccountId: string },
|
18
18
|
data: Partial<UserItem>,
|
19
19
|
) => {
|
20
|
-
pino.info(
|
20
|
+
pino.info(`updating user "${JSON.stringify({ provider, providerAccountId })}" due to webhook`);
|
21
21
|
// 1. Find User by account
|
22
22
|
// @ts-expect-error: Already impl in `LobeNextauthDbAdapter`
|
23
23
|
const user = await this.adapter.getUserByAccount({
|
@@ -37,7 +37,7 @@ export class NextAuthUserService {
|
|
37
37
|
});
|
38
38
|
} else {
|
39
39
|
pino.warn(
|
40
|
-
`[${provider}]: Webhooks handler user update for "${JSON.stringify(data)}", but no user was found by the providerAccountId.`,
|
40
|
+
`[${provider}]: Webhooks handler user "${JSON.stringify({ provider, providerAccountId })}" update for "${JSON.stringify(data)}", but no user was found by the providerAccountId.`,
|
41
41
|
);
|
42
42
|
}
|
43
43
|
return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
2
|
|
3
3
|
import { AgentRuntimeErrorType } from '@/libs/agent-runtime';
|
4
4
|
import { ChatErrorType } from '@/types/fetch';
|
@@ -32,6 +32,30 @@ describe('createErrorResponse', () => {
|
|
32
32
|
expect(response.status).toBe(403);
|
33
33
|
});
|
34
34
|
|
35
|
+
it('returns a 429 status for InsufficientQuota error type', () => {
|
36
|
+
const errorType = AgentRuntimeErrorType.InsufficientQuota;
|
37
|
+
const response = createErrorResponse(errorType);
|
38
|
+
expect(response.status).toBe(429);
|
39
|
+
});
|
40
|
+
|
41
|
+
it('returns a 429 status for QuotaLimitReached error type', () => {
|
42
|
+
const errorType = AgentRuntimeErrorType.QuotaLimitReached;
|
43
|
+
const response = createErrorResponse(errorType);
|
44
|
+
expect(response.status).toBe(429);
|
45
|
+
});
|
46
|
+
|
47
|
+
it('returns a 400 status for ExceededContextWindow error type', () => {
|
48
|
+
const errorType = AgentRuntimeErrorType.ExceededContextWindow;
|
49
|
+
const response = createErrorResponse(errorType);
|
50
|
+
expect(response.status).toBe(400);
|
51
|
+
});
|
52
|
+
|
53
|
+
it('returns a 400 status for SystemTimeNotMatchError error type', () => {
|
54
|
+
const errorType = ChatErrorType.SystemTimeNotMatchError;
|
55
|
+
const response = createErrorResponse(errorType);
|
56
|
+
expect(response.status).toBe(400);
|
57
|
+
});
|
58
|
+
|
35
59
|
describe('Provider Biz Error', () => {
|
36
60
|
it('returns a 471 status for ProviderBizError error type', () => {
|
37
61
|
const errorType = AgentRuntimeErrorType.ProviderBizError;
|
@@ -44,6 +68,18 @@ describe('createErrorResponse', () => {
|
|
44
68
|
const response = createErrorResponse(errorType);
|
45
69
|
expect(response.status).toBe(470);
|
46
70
|
});
|
71
|
+
|
72
|
+
it('returns a 472 status for OllamaBizError error type', () => {
|
73
|
+
const errorType = AgentRuntimeErrorType.OllamaBizError;
|
74
|
+
const response = createErrorResponse(errorType);
|
75
|
+
expect(response.status).toBe(472);
|
76
|
+
});
|
77
|
+
|
78
|
+
it('returns a 472 status for OllamaServiceUnavailable error type', () => {
|
79
|
+
const errorType = ChatErrorType.OllamaServiceUnavailable;
|
80
|
+
const response = createErrorResponse(errorType);
|
81
|
+
expect(response.status).toBe(472);
|
82
|
+
});
|
47
83
|
});
|
48
84
|
|
49
85
|
// 测试状态码不在200-599范围内的情况
|