@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.138
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/CLAUDE.md +38 -0
- package/changelog/v1.json +5 -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,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
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { ThreadStatus, ThreadType } from '@lobechat/types';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { sessions, threads, topics, users } from '../../schemas';
|
|
6
|
+
import { LobeChatDatabase } from '../../type';
|
|
7
|
+
import { ThreadModel } from '../thread';
|
|
8
|
+
import { getTestDB } from './_util';
|
|
9
|
+
|
|
10
|
+
const userId = 'thread-user-test';
|
|
11
|
+
const otherUserId = 'other-user-test';
|
|
12
|
+
const sessionId = 'thread-session';
|
|
13
|
+
const topicId = 'thread-topic';
|
|
14
|
+
|
|
15
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
16
|
+
const threadModel = new ThreadModel(serverDB, userId);
|
|
17
|
+
|
|
18
|
+
describe('ThreadModel', () => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await serverDB.delete(users);
|
|
21
|
+
|
|
22
|
+
// Create test users, session and topic
|
|
23
|
+
await serverDB.transaction(async (tx) => {
|
|
24
|
+
await tx.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
25
|
+
await tx.insert(sessions).values({ id: sessionId, userId });
|
|
26
|
+
await tx.insert(topics).values({ id: topicId, userId, sessionId });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await serverDB.delete(users);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('create', () => {
|
|
35
|
+
it('should create a new thread', async () => {
|
|
36
|
+
const result = await threadModel.create({
|
|
37
|
+
topicId,
|
|
38
|
+
type: ThreadType.Standalone,
|
|
39
|
+
sourceMessageId: 'msg-1',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result).toBeDefined();
|
|
43
|
+
expect(result.topicId).toBe(topicId);
|
|
44
|
+
expect(result.type).toBe(ThreadType.Standalone);
|
|
45
|
+
expect(result.status).toBe(ThreadStatus.Active);
|
|
46
|
+
expect(result.sourceMessageId).toBe('msg-1');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should create a thread with title', async () => {
|
|
50
|
+
const result = await threadModel.create({
|
|
51
|
+
topicId,
|
|
52
|
+
type: ThreadType.Continuation,
|
|
53
|
+
title: 'Test Thread',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.title).toBe('Test Thread');
|
|
57
|
+
expect(result.type).toBe(ThreadType.Continuation);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('query', () => {
|
|
62
|
+
it('should return all threads for the user', async () => {
|
|
63
|
+
// Create test threads
|
|
64
|
+
await serverDB.insert(threads).values([
|
|
65
|
+
{
|
|
66
|
+
id: 'thread-1',
|
|
67
|
+
topicId,
|
|
68
|
+
type: ThreadType.Standalone,
|
|
69
|
+
status: ThreadStatus.Active,
|
|
70
|
+
userId,
|
|
71
|
+
updatedAt: new Date('2024-01-01'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'thread-2',
|
|
75
|
+
topicId,
|
|
76
|
+
type: ThreadType.Continuation,
|
|
77
|
+
status: ThreadStatus.Active,
|
|
78
|
+
userId,
|
|
79
|
+
updatedAt: new Date('2024-01-02'),
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const result = await threadModel.query();
|
|
84
|
+
|
|
85
|
+
expect(result).toHaveLength(2);
|
|
86
|
+
// Should be ordered by updatedAt desc
|
|
87
|
+
expect(result[0].id).toBe('thread-2');
|
|
88
|
+
expect(result[1].id).toBe('thread-1');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should only return threads for the current user', async () => {
|
|
92
|
+
await serverDB.transaction(async (tx) => {
|
|
93
|
+
await tx.insert(topics).values({ id: 'other-topic', userId: otherUserId });
|
|
94
|
+
await tx.insert(threads).values([
|
|
95
|
+
{
|
|
96
|
+
id: 'thread-1',
|
|
97
|
+
topicId,
|
|
98
|
+
type: ThreadType.Standalone,
|
|
99
|
+
status: ThreadStatus.Active,
|
|
100
|
+
userId,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'thread-2',
|
|
104
|
+
topicId: 'other-topic',
|
|
105
|
+
type: ThreadType.Standalone,
|
|
106
|
+
status: ThreadStatus.Active,
|
|
107
|
+
userId: otherUserId,
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await threadModel.query();
|
|
113
|
+
|
|
114
|
+
expect(result).toHaveLength(1);
|
|
115
|
+
expect(result[0].id).toBe('thread-1');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('queryByTopicId', () => {
|
|
120
|
+
it('should return threads for a specific topic', async () => {
|
|
121
|
+
await serverDB.transaction(async (tx) => {
|
|
122
|
+
await tx.insert(topics).values({ id: 'another-topic', userId, sessionId });
|
|
123
|
+
await tx.insert(threads).values([
|
|
124
|
+
{
|
|
125
|
+
id: 'thread-1',
|
|
126
|
+
topicId,
|
|
127
|
+
type: ThreadType.Standalone,
|
|
128
|
+
status: ThreadStatus.Active,
|
|
129
|
+
userId,
|
|
130
|
+
updatedAt: new Date('2024-01-01'),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'thread-2',
|
|
134
|
+
topicId: 'another-topic',
|
|
135
|
+
type: ThreadType.Standalone,
|
|
136
|
+
status: ThreadStatus.Active,
|
|
137
|
+
userId,
|
|
138
|
+
updatedAt: new Date('2024-01-02'),
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await threadModel.queryByTopicId(topicId);
|
|
144
|
+
|
|
145
|
+
expect(result).toHaveLength(1);
|
|
146
|
+
expect(result[0].id).toBe('thread-1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return empty array when no threads exist for the topic', async () => {
|
|
150
|
+
const result = await threadModel.queryByTopicId('non-existent-topic');
|
|
151
|
+
|
|
152
|
+
expect(result).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('findById', () => {
|
|
157
|
+
it('should return a thread by id', async () => {
|
|
158
|
+
await serverDB.insert(threads).values({
|
|
159
|
+
id: 'thread-1',
|
|
160
|
+
topicId,
|
|
161
|
+
type: ThreadType.Standalone,
|
|
162
|
+
status: ThreadStatus.Active,
|
|
163
|
+
userId,
|
|
164
|
+
title: 'Test Thread',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = await threadModel.findById('thread-1');
|
|
168
|
+
|
|
169
|
+
expect(result).toBeDefined();
|
|
170
|
+
expect(result?.id).toBe('thread-1');
|
|
171
|
+
expect(result?.title).toBe('Test Thread');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return undefined for non-existent thread', async () => {
|
|
175
|
+
const result = await threadModel.findById('non-existent');
|
|
176
|
+
|
|
177
|
+
expect(result).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should not return thread belonging to another user', async () => {
|
|
181
|
+
await serverDB.transaction(async (tx) => {
|
|
182
|
+
await tx.insert(topics).values({ id: 'other-topic', userId: otherUserId });
|
|
183
|
+
await tx.insert(threads).values({
|
|
184
|
+
id: 'thread-other',
|
|
185
|
+
topicId: 'other-topic',
|
|
186
|
+
type: ThreadType.Standalone,
|
|
187
|
+
status: ThreadStatus.Active,
|
|
188
|
+
userId: otherUserId,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const result = await threadModel.findById('thread-other');
|
|
193
|
+
|
|
194
|
+
expect(result).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('update', () => {
|
|
199
|
+
it('should update a thread', async () => {
|
|
200
|
+
await serverDB.insert(threads).values({
|
|
201
|
+
id: 'thread-1',
|
|
202
|
+
topicId,
|
|
203
|
+
type: ThreadType.Standalone,
|
|
204
|
+
status: ThreadStatus.Active,
|
|
205
|
+
userId,
|
|
206
|
+
title: 'Original Title',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await threadModel.update('thread-1', {
|
|
210
|
+
title: 'Updated Title',
|
|
211
|
+
status: ThreadStatus.Archived,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const updated = await serverDB.query.threads.findFirst({
|
|
215
|
+
where: eq(threads.id, 'thread-1'),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(updated?.title).toBe('Updated Title');
|
|
219
|
+
expect(updated?.status).toBe(ThreadStatus.Archived);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should not update thread belonging to another user', async () => {
|
|
223
|
+
await serverDB.transaction(async (tx) => {
|
|
224
|
+
await tx.insert(topics).values({ id: 'other-topic', userId: otherUserId });
|
|
225
|
+
await tx.insert(threads).values({
|
|
226
|
+
id: 'thread-other',
|
|
227
|
+
topicId: 'other-topic',
|
|
228
|
+
type: ThreadType.Standalone,
|
|
229
|
+
status: ThreadStatus.Active,
|
|
230
|
+
userId: otherUserId,
|
|
231
|
+
title: 'Original Title',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await threadModel.update('thread-other', { title: 'Hacked Title' });
|
|
236
|
+
|
|
237
|
+
const unchanged = await serverDB.query.threads.findFirst({
|
|
238
|
+
where: eq(threads.id, 'thread-other'),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(unchanged?.title).toBe('Original Title');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('delete', () => {
|
|
246
|
+
it('should delete a thread', async () => {
|
|
247
|
+
await serverDB.insert(threads).values({
|
|
248
|
+
id: 'thread-1',
|
|
249
|
+
topicId,
|
|
250
|
+
type: ThreadType.Standalone,
|
|
251
|
+
status: ThreadStatus.Active,
|
|
252
|
+
userId,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await threadModel.delete('thread-1');
|
|
256
|
+
|
|
257
|
+
const deleted = await serverDB.query.threads.findFirst({
|
|
258
|
+
where: eq(threads.id, 'thread-1'),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(deleted).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should not delete thread belonging to another user', async () => {
|
|
265
|
+
await serverDB.transaction(async (tx) => {
|
|
266
|
+
await tx.insert(topics).values({ id: 'other-topic', userId: otherUserId });
|
|
267
|
+
await tx.insert(threads).values({
|
|
268
|
+
id: 'thread-other',
|
|
269
|
+
topicId: 'other-topic',
|
|
270
|
+
type: ThreadType.Standalone,
|
|
271
|
+
status: ThreadStatus.Active,
|
|
272
|
+
userId: otherUserId,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await threadModel.delete('thread-other');
|
|
277
|
+
|
|
278
|
+
const stillExists = await serverDB.query.threads.findFirst({
|
|
279
|
+
where: eq(threads.id, 'thread-other'),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(stillExists).toBeDefined();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('deleteAll', () => {
|
|
287
|
+
it('should delete all threads for the current user', async () => {
|
|
288
|
+
await serverDB.transaction(async (tx) => {
|
|
289
|
+
await tx.insert(topics).values({ id: 'other-topic', userId: otherUserId });
|
|
290
|
+
await tx.insert(threads).values([
|
|
291
|
+
{
|
|
292
|
+
id: 'thread-1',
|
|
293
|
+
topicId,
|
|
294
|
+
type: ThreadType.Standalone,
|
|
295
|
+
status: ThreadStatus.Active,
|
|
296
|
+
userId,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 'thread-2',
|
|
300
|
+
topicId,
|
|
301
|
+
type: ThreadType.Continuation,
|
|
302
|
+
status: ThreadStatus.Active,
|
|
303
|
+
userId,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: 'thread-3',
|
|
307
|
+
topicId: 'other-topic',
|
|
308
|
+
type: ThreadType.Standalone,
|
|
309
|
+
status: ThreadStatus.Active,
|
|
310
|
+
userId: otherUserId,
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await threadModel.deleteAll();
|
|
316
|
+
|
|
317
|
+
const userThreads = await serverDB.select().from(threads).where(eq(threads.userId, userId));
|
|
318
|
+
const otherUserThreads = await serverDB
|
|
319
|
+
.select()
|
|
320
|
+
.from(threads)
|
|
321
|
+
.where(eq(threads.userId, otherUserId));
|
|
322
|
+
|
|
323
|
+
expect(userThreads).toHaveLength(0);
|
|
324
|
+
expect(otherUserThreads).toHaveLength(1);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|