@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.139
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 +50 -0
- package/CLAUDE.md +38 -0
- package/changelog/v1.json +14 -0
- package/locales/ar/modelProvider.json +2 -0
- package/locales/bg-BG/modelProvider.json +2 -0
- package/locales/de-DE/modelProvider.json +2 -0
- package/locales/en-US/modelProvider.json +2 -0
- package/locales/es-ES/modelProvider.json +2 -0
- package/locales/fa-IR/modelProvider.json +2 -0
- package/locales/fr-FR/modelProvider.json +2 -0
- package/locales/it-IT/modelProvider.json +2 -0
- package/locales/ja-JP/modelProvider.json +2 -0
- package/locales/ko-KR/modelProvider.json +2 -0
- package/locales/nl-NL/modelProvider.json +2 -0
- package/locales/pl-PL/modelProvider.json +2 -0
- package/locales/pt-BR/modelProvider.json +2 -0
- package/locales/ru-RU/modelProvider.json +2 -0
- package/locales/tr-TR/modelProvider.json +2 -0
- package/locales/vi-VN/modelProvider.json +2 -0
- package/locales/zh-CN/modelProvider.json +2 -0
- package/locales/zh-TW/modelProvider.json +2 -0
- package/package.json +1 -1
- package/packages/conversation-flow/src/transformation/BranchResolver.ts +24 -14
- package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +6 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +15 -0
- package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +66 -3
- package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +64 -0
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +47 -0
- package/packages/database/src/models/__tests__/agent.test.ts +102 -3
- package/packages/database/src/models/__tests__/document.test.ts +163 -0
- package/packages/database/src/models/__tests__/embedding.test.ts +294 -0
- package/packages/database/src/models/__tests__/oauthHandoff.test.ts +261 -0
- package/packages/database/src/models/__tests__/thread.test.ts +327 -0
- package/packages/database/src/models/__tests__/user.test.ts +372 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { chunks, embeddings, users } from '../../schemas';
|
|
5
|
+
import { LobeChatDatabase } from '../../type';
|
|
6
|
+
import { EmbeddingModel } from '../embedding';
|
|
7
|
+
import { getTestDB } from './_util';
|
|
8
|
+
import { designThinkingQuery } from './fixtures/embedding';
|
|
9
|
+
|
|
10
|
+
const userId = 'embedding-user-test';
|
|
11
|
+
const otherUserId = 'other-user-test';
|
|
12
|
+
|
|
13
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
14
|
+
const embeddingModel = new EmbeddingModel(serverDB, userId);
|
|
15
|
+
|
|
16
|
+
describe('EmbeddingModel', () => {
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await serverDB.delete(users);
|
|
19
|
+
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await serverDB.delete(users);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('create', () => {
|
|
27
|
+
it('should create a new embedding', async () => {
|
|
28
|
+
// Create a chunk first
|
|
29
|
+
const [chunk] = await serverDB
|
|
30
|
+
.insert(chunks)
|
|
31
|
+
.values({ text: 'Test chunk', userId })
|
|
32
|
+
.returning();
|
|
33
|
+
|
|
34
|
+
const id = await embeddingModel.create({
|
|
35
|
+
chunkId: chunk.id,
|
|
36
|
+
embeddings: designThinkingQuery,
|
|
37
|
+
model: 'text-embedding-ada-002',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(id).toBeDefined();
|
|
41
|
+
|
|
42
|
+
const created = await serverDB.query.embeddings.findFirst({
|
|
43
|
+
where: eq(embeddings.id, id),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(created).toBeDefined();
|
|
47
|
+
expect(created?.chunkId).toBe(chunk.id);
|
|
48
|
+
expect(created?.model).toBe('text-embedding-ada-002');
|
|
49
|
+
expect(created?.userId).toBe(userId);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('bulkCreate', () => {
|
|
54
|
+
it('should create multiple embeddings', async () => {
|
|
55
|
+
// Create chunks first
|
|
56
|
+
const [chunk1, chunk2] = await serverDB
|
|
57
|
+
.insert(chunks)
|
|
58
|
+
.values([
|
|
59
|
+
{ text: 'Test chunk 1', userId },
|
|
60
|
+
{ text: 'Test chunk 2', userId },
|
|
61
|
+
])
|
|
62
|
+
.returning();
|
|
63
|
+
|
|
64
|
+
await embeddingModel.bulkCreate([
|
|
65
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, model: 'text-embedding-ada-002' },
|
|
66
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, model: 'text-embedding-ada-002' },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const created = await serverDB.query.embeddings.findMany({
|
|
70
|
+
where: eq(embeddings.userId, userId),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(created).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle duplicate chunkId with onConflictDoNothing', async () => {
|
|
77
|
+
// Create a chunk
|
|
78
|
+
const [chunk] = await serverDB
|
|
79
|
+
.insert(chunks)
|
|
80
|
+
.values({ text: 'Test chunk', userId })
|
|
81
|
+
.returning();
|
|
82
|
+
|
|
83
|
+
// Create first embedding
|
|
84
|
+
await embeddingModel.create({
|
|
85
|
+
chunkId: chunk.id,
|
|
86
|
+
embeddings: designThinkingQuery,
|
|
87
|
+
model: 'text-embedding-ada-002',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Try to create duplicate
|
|
91
|
+
await embeddingModel.bulkCreate([
|
|
92
|
+
{ chunkId: chunk.id, embeddings: designThinkingQuery, model: 'text-embedding-3-small' },
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const created = await serverDB.query.embeddings.findMany({
|
|
96
|
+
where: eq(embeddings.chunkId, chunk.id),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Should still have only 1 embedding due to unique constraint
|
|
100
|
+
expect(created).toHaveLength(1);
|
|
101
|
+
expect(created[0].model).toBe('text-embedding-ada-002');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('delete', () => {
|
|
106
|
+
it('should delete an embedding', async () => {
|
|
107
|
+
const [chunk] = await serverDB
|
|
108
|
+
.insert(chunks)
|
|
109
|
+
.values({ text: 'Test chunk', userId })
|
|
110
|
+
.returning();
|
|
111
|
+
|
|
112
|
+
const id = await embeddingModel.create({
|
|
113
|
+
chunkId: chunk.id,
|
|
114
|
+
embeddings: designThinkingQuery,
|
|
115
|
+
model: 'text-embedding-ada-002',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await embeddingModel.delete(id);
|
|
119
|
+
|
|
120
|
+
const deleted = await serverDB.query.embeddings.findFirst({
|
|
121
|
+
where: eq(embeddings.id, id),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(deleted).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should not delete embedding belonging to another user', async () => {
|
|
128
|
+
// Create chunk and embedding for other user
|
|
129
|
+
const [chunk] = await serverDB
|
|
130
|
+
.insert(chunks)
|
|
131
|
+
.values({ text: 'Other user chunk', userId: otherUserId })
|
|
132
|
+
.returning();
|
|
133
|
+
|
|
134
|
+
const [otherEmbedding] = await serverDB
|
|
135
|
+
.insert(embeddings)
|
|
136
|
+
.values({
|
|
137
|
+
chunkId: chunk.id,
|
|
138
|
+
embeddings: designThinkingQuery,
|
|
139
|
+
model: 'text-embedding-ada-002',
|
|
140
|
+
userId: otherUserId,
|
|
141
|
+
})
|
|
142
|
+
.returning();
|
|
143
|
+
|
|
144
|
+
await embeddingModel.delete(otherEmbedding.id);
|
|
145
|
+
|
|
146
|
+
const stillExists = await serverDB.query.embeddings.findFirst({
|
|
147
|
+
where: eq(embeddings.id, otherEmbedding.id),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(stillExists).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('query', () => {
|
|
155
|
+
it('should return all embeddings for the user', async () => {
|
|
156
|
+
const [chunk1, chunk2] = await serverDB
|
|
157
|
+
.insert(chunks)
|
|
158
|
+
.values([
|
|
159
|
+
{ text: 'Test chunk 1', userId },
|
|
160
|
+
{ text: 'Test chunk 2', userId },
|
|
161
|
+
])
|
|
162
|
+
.returning();
|
|
163
|
+
|
|
164
|
+
await serverDB.insert(embeddings).values([
|
|
165
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
166
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const result = await embeddingModel.query();
|
|
170
|
+
|
|
171
|
+
expect(result).toHaveLength(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should only return embeddings for the current user', async () => {
|
|
175
|
+
const [chunk1] = await serverDB
|
|
176
|
+
.insert(chunks)
|
|
177
|
+
.values([{ text: 'Test chunk 1', userId }])
|
|
178
|
+
.returning();
|
|
179
|
+
|
|
180
|
+
const [chunk2] = await serverDB
|
|
181
|
+
.insert(chunks)
|
|
182
|
+
.values([{ text: 'Other user chunk', userId: otherUserId }])
|
|
183
|
+
.returning();
|
|
184
|
+
|
|
185
|
+
await serverDB.insert(embeddings).values([
|
|
186
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
187
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId: otherUserId },
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const result = await embeddingModel.query();
|
|
191
|
+
|
|
192
|
+
expect(result).toHaveLength(1);
|
|
193
|
+
expect(result[0].userId).toBe(userId);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('findById', () => {
|
|
198
|
+
it('should return an embedding by id', async () => {
|
|
199
|
+
const [chunk] = await serverDB
|
|
200
|
+
.insert(chunks)
|
|
201
|
+
.values({ text: 'Test chunk', userId })
|
|
202
|
+
.returning();
|
|
203
|
+
|
|
204
|
+
const id = await embeddingModel.create({
|
|
205
|
+
chunkId: chunk.id,
|
|
206
|
+
embeddings: designThinkingQuery,
|
|
207
|
+
model: 'text-embedding-ada-002',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = await embeddingModel.findById(id);
|
|
211
|
+
|
|
212
|
+
expect(result).toBeDefined();
|
|
213
|
+
expect(result?.id).toBe(id);
|
|
214
|
+
expect(result?.chunkId).toBe(chunk.id);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return undefined for non-existent embedding', async () => {
|
|
218
|
+
// Use a valid UUID format that doesn't exist
|
|
219
|
+
const result = await embeddingModel.findById('00000000-0000-0000-0000-000000000000');
|
|
220
|
+
|
|
221
|
+
expect(result).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should not return embedding belonging to another user', async () => {
|
|
225
|
+
const [chunk] = await serverDB
|
|
226
|
+
.insert(chunks)
|
|
227
|
+
.values({ text: 'Other user chunk', userId: otherUserId })
|
|
228
|
+
.returning();
|
|
229
|
+
|
|
230
|
+
const [otherEmbedding] = await serverDB
|
|
231
|
+
.insert(embeddings)
|
|
232
|
+
.values({
|
|
233
|
+
chunkId: chunk.id,
|
|
234
|
+
embeddings: designThinkingQuery,
|
|
235
|
+
userId: otherUserId,
|
|
236
|
+
})
|
|
237
|
+
.returning();
|
|
238
|
+
|
|
239
|
+
const result = await embeddingModel.findById(otherEmbedding.id);
|
|
240
|
+
|
|
241
|
+
expect(result).toBeUndefined();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('countUsage', () => {
|
|
246
|
+
it('should return the count of embeddings for the user', async () => {
|
|
247
|
+
const [chunk1, chunk2, chunk3] = await serverDB
|
|
248
|
+
.insert(chunks)
|
|
249
|
+
.values([
|
|
250
|
+
{ text: 'Test chunk 1', userId },
|
|
251
|
+
{ text: 'Test chunk 2', userId },
|
|
252
|
+
{ text: 'Test chunk 3', userId },
|
|
253
|
+
])
|
|
254
|
+
.returning();
|
|
255
|
+
|
|
256
|
+
await serverDB.insert(embeddings).values([
|
|
257
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
258
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId },
|
|
259
|
+
{ chunkId: chunk3.id, embeddings: designThinkingQuery, userId },
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const count = await embeddingModel.countUsage();
|
|
263
|
+
|
|
264
|
+
expect(count).toBe(3);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return 0 when user has no embeddings', async () => {
|
|
268
|
+
const count = await embeddingModel.countUsage();
|
|
269
|
+
|
|
270
|
+
expect(count).toBe(0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should only count embeddings for the current user', async () => {
|
|
274
|
+
const [chunk1] = await serverDB
|
|
275
|
+
.insert(chunks)
|
|
276
|
+
.values([{ text: 'Test chunk 1', userId }])
|
|
277
|
+
.returning();
|
|
278
|
+
|
|
279
|
+
const [chunk2] = await serverDB
|
|
280
|
+
.insert(chunks)
|
|
281
|
+
.values([{ text: 'Other user chunk', userId: otherUserId }])
|
|
282
|
+
.returning();
|
|
283
|
+
|
|
284
|
+
await serverDB.insert(embeddings).values([
|
|
285
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
286
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId: otherUserId },
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const count = await embeddingModel.countUsage();
|
|
290
|
+
|
|
291
|
+
expect(count).toBe(1);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { oauthHandoffs } from '../../schemas';
|
|
5
|
+
import { LobeChatDatabase } from '../../type';
|
|
6
|
+
import { OAuthHandoffModel } from '../oauthHandoff';
|
|
7
|
+
import { getTestDB } from './_util';
|
|
8
|
+
|
|
9
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
10
|
+
const oauthHandoffModel = new OAuthHandoffModel(serverDB);
|
|
11
|
+
|
|
12
|
+
describe('OAuthHandoffModel', () => {
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await serverDB.delete(oauthHandoffs);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await serverDB.delete(oauthHandoffs);
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('create', () => {
|
|
23
|
+
it('should create a new OAuth handoff record', async () => {
|
|
24
|
+
const result = await oauthHandoffModel.create({
|
|
25
|
+
id: 'handoff-1',
|
|
26
|
+
client: 'desktop',
|
|
27
|
+
payload: { code: 'auth-code', state: 'state-value' },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result).toBeDefined();
|
|
31
|
+
expect(result.id).toBe('handoff-1');
|
|
32
|
+
expect(result.client).toBe('desktop');
|
|
33
|
+
expect(result.payload).toEqual({ code: 'auth-code', state: 'state-value' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle conflict by doing nothing', async () => {
|
|
37
|
+
// Create first record
|
|
38
|
+
await oauthHandoffModel.create({
|
|
39
|
+
id: 'handoff-1',
|
|
40
|
+
client: 'desktop',
|
|
41
|
+
payload: { code: 'original-code' },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Try to create duplicate - should not throw
|
|
45
|
+
const result = await oauthHandoffModel.create({
|
|
46
|
+
id: 'handoff-1',
|
|
47
|
+
client: 'desktop',
|
|
48
|
+
payload: { code: 'new-code' },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Result is undefined when conflict occurs with onConflictDoNothing
|
|
52
|
+
expect(result).toBeUndefined();
|
|
53
|
+
|
|
54
|
+
// Original should remain unchanged
|
|
55
|
+
const record = await serverDB.query.oauthHandoffs.findFirst({
|
|
56
|
+
where: eq(oauthHandoffs.id, 'handoff-1'),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(record?.payload).toEqual({ code: 'original-code' });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('fetchAndConsume', () => {
|
|
64
|
+
it('should fetch and delete valid credentials', async () => {
|
|
65
|
+
await oauthHandoffModel.create({
|
|
66
|
+
id: 'handoff-1',
|
|
67
|
+
client: 'desktop',
|
|
68
|
+
payload: { code: 'auth-code', state: 'state-value' },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await oauthHandoffModel.fetchAndConsume('handoff-1', 'desktop');
|
|
72
|
+
|
|
73
|
+
expect(result).toBeDefined();
|
|
74
|
+
expect(result?.id).toBe('handoff-1');
|
|
75
|
+
expect(result?.payload).toEqual({ code: 'auth-code', state: 'state-value' });
|
|
76
|
+
|
|
77
|
+
// Verify it was deleted
|
|
78
|
+
const deleted = await serverDB.query.oauthHandoffs.findFirst({
|
|
79
|
+
where: eq(oauthHandoffs.id, 'handoff-1'),
|
|
80
|
+
});
|
|
81
|
+
expect(deleted).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return null for non-existent credentials', async () => {
|
|
85
|
+
const result = await oauthHandoffModel.fetchAndConsume('non-existent', 'desktop');
|
|
86
|
+
|
|
87
|
+
expect(result).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return null when client type does not match', async () => {
|
|
91
|
+
await oauthHandoffModel.create({
|
|
92
|
+
id: 'handoff-1',
|
|
93
|
+
client: 'desktop',
|
|
94
|
+
payload: { code: 'auth-code' },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await oauthHandoffModel.fetchAndConsume('handoff-1', 'browser-extension');
|
|
98
|
+
|
|
99
|
+
expect(result).toBeNull();
|
|
100
|
+
|
|
101
|
+
// Record should still exist
|
|
102
|
+
const record = await serverDB.query.oauthHandoffs.findFirst({
|
|
103
|
+
where: eq(oauthHandoffs.id, 'handoff-1'),
|
|
104
|
+
});
|
|
105
|
+
expect(record).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return null for expired credentials (older than 5 minutes)', async () => {
|
|
109
|
+
// Create a record with old timestamp
|
|
110
|
+
const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000);
|
|
111
|
+
|
|
112
|
+
await serverDB.insert(oauthHandoffs).values({
|
|
113
|
+
id: 'handoff-expired',
|
|
114
|
+
client: 'desktop',
|
|
115
|
+
payload: { code: 'auth-code' },
|
|
116
|
+
createdAt: sixMinutesAgo,
|
|
117
|
+
updatedAt: sixMinutesAgo,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await oauthHandoffModel.fetchAndConsume('handoff-expired', 'desktop');
|
|
121
|
+
|
|
122
|
+
expect(result).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return valid credentials within 5 minutes', async () => {
|
|
126
|
+
// Create a record with timestamp 4 minutes ago
|
|
127
|
+
const fourMinutesAgo = new Date(Date.now() - 4 * 60 * 1000);
|
|
128
|
+
|
|
129
|
+
await serverDB.insert(oauthHandoffs).values({
|
|
130
|
+
id: 'handoff-valid',
|
|
131
|
+
client: 'desktop',
|
|
132
|
+
payload: { code: 'auth-code' },
|
|
133
|
+
createdAt: fourMinutesAgo,
|
|
134
|
+
updatedAt: fourMinutesAgo,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = await oauthHandoffModel.fetchAndConsume('handoff-valid', 'desktop');
|
|
138
|
+
|
|
139
|
+
expect(result).toBeDefined();
|
|
140
|
+
expect(result?.id).toBe('handoff-valid');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('cleanupExpired', () => {
|
|
145
|
+
it('should delete expired records', async () => {
|
|
146
|
+
const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000);
|
|
147
|
+
const now = new Date();
|
|
148
|
+
|
|
149
|
+
// Create expired record
|
|
150
|
+
await serverDB.insert(oauthHandoffs).values({
|
|
151
|
+
id: 'handoff-expired',
|
|
152
|
+
client: 'desktop',
|
|
153
|
+
payload: { code: 'expired-code' },
|
|
154
|
+
createdAt: sixMinutesAgo,
|
|
155
|
+
updatedAt: sixMinutesAgo,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Create valid record
|
|
159
|
+
await serverDB.insert(oauthHandoffs).values({
|
|
160
|
+
id: 'handoff-valid',
|
|
161
|
+
client: 'desktop',
|
|
162
|
+
payload: { code: 'valid-code' },
|
|
163
|
+
createdAt: now,
|
|
164
|
+
updatedAt: now,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await oauthHandoffModel.cleanupExpired();
|
|
168
|
+
|
|
169
|
+
// Verify expired record is deleted
|
|
170
|
+
const expiredRecord = await serverDB.query.oauthHandoffs.findFirst({
|
|
171
|
+
where: eq(oauthHandoffs.id, 'handoff-expired'),
|
|
172
|
+
});
|
|
173
|
+
expect(expiredRecord).toBeUndefined();
|
|
174
|
+
|
|
175
|
+
// Verify valid record still exists
|
|
176
|
+
const validRecord = await serverDB.query.oauthHandoffs.findFirst({
|
|
177
|
+
where: eq(oauthHandoffs.id, 'handoff-valid'),
|
|
178
|
+
});
|
|
179
|
+
expect(validRecord).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should not throw when no expired records exist', async () => {
|
|
183
|
+
await expect(oauthHandoffModel.cleanupExpired()).resolves.not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should delete multiple expired records', async () => {
|
|
187
|
+
const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000);
|
|
188
|
+
|
|
189
|
+
await serverDB.insert(oauthHandoffs).values([
|
|
190
|
+
{
|
|
191
|
+
id: 'handoff-expired-1',
|
|
192
|
+
client: 'desktop',
|
|
193
|
+
payload: { code: 'code-1' },
|
|
194
|
+
createdAt: sixMinutesAgo,
|
|
195
|
+
updatedAt: sixMinutesAgo,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'handoff-expired-2',
|
|
199
|
+
client: 'browser',
|
|
200
|
+
payload: { code: 'code-2' },
|
|
201
|
+
createdAt: sixMinutesAgo,
|
|
202
|
+
updatedAt: sixMinutesAgo,
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
await oauthHandoffModel.cleanupExpired();
|
|
207
|
+
|
|
208
|
+
// Verify all expired records are deleted
|
|
209
|
+
const remainingRecords = await serverDB.query.oauthHandoffs.findMany();
|
|
210
|
+
expect(remainingRecords).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('exists', () => {
|
|
215
|
+
it('should return true for existing valid credentials', async () => {
|
|
216
|
+
await oauthHandoffModel.create({
|
|
217
|
+
id: 'handoff-1',
|
|
218
|
+
client: 'desktop',
|
|
219
|
+
payload: { code: 'auth-code' },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const result = await oauthHandoffModel.exists('handoff-1', 'desktop');
|
|
223
|
+
|
|
224
|
+
expect(result).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return false for non-existent credentials', async () => {
|
|
228
|
+
const result = await oauthHandoffModel.exists('non-existent', 'desktop');
|
|
229
|
+
|
|
230
|
+
expect(result).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should return false when client type does not match', async () => {
|
|
234
|
+
await oauthHandoffModel.create({
|
|
235
|
+
id: 'handoff-1',
|
|
236
|
+
client: 'desktop',
|
|
237
|
+
payload: { code: 'auth-code' },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const result = await oauthHandoffModel.exists('handoff-1', 'browser-extension');
|
|
241
|
+
|
|
242
|
+
expect(result).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should return false for expired credentials', async () => {
|
|
246
|
+
const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000);
|
|
247
|
+
|
|
248
|
+
await serverDB.insert(oauthHandoffs).values({
|
|
249
|
+
id: 'handoff-expired',
|
|
250
|
+
client: 'desktop',
|
|
251
|
+
payload: { code: 'auth-code' },
|
|
252
|
+
createdAt: sixMinutesAgo,
|
|
253
|
+
updatedAt: sixMinutesAgo,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = await oauthHandoffModel.exists('handoff-expired', 'desktop');
|
|
257
|
+
|
|
258
|
+
expect(result).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|