@lobehub/lobehub 2.0.0-next.141 → 2.0.0-next.142
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/Dockerfile +2 -0
- package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
- package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
- package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
- package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
- package/changelog/v1.json +9 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
- package/package.json +1 -1
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
- package/src/config/modelProviders/vertexai.ts +1 -1
- package/src/envs/llm.ts +4 -0
- package/src/server/modules/ModelRuntime/index.ts +4 -4
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { getPort } from 'get-port-please';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import type { App } from '../../App';
|
|
6
|
+
import { StaticFileServerManager } from '../StaticFileServerManager';
|
|
7
|
+
|
|
8
|
+
// Mock get-port-please
|
|
9
|
+
vi.mock('get-port-please', () => ({
|
|
10
|
+
getPort: vi.fn().mockResolvedValue(33250),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Create mock server and handler storage
|
|
14
|
+
const mockServerHandler = { current: null as any };
|
|
15
|
+
const mockServer = {
|
|
16
|
+
close: vi.fn((cb?: () => void) => cb?.()),
|
|
17
|
+
listen: vi.fn((_port: number, _host: string, cb: () => void) => cb()),
|
|
18
|
+
on: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Mock node:http
|
|
22
|
+
vi.mock('node:http', () => ({
|
|
23
|
+
createServer: vi.fn((handler: any) => {
|
|
24
|
+
mockServerHandler.current = handler;
|
|
25
|
+
return mockServer;
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock logger
|
|
30
|
+
vi.mock('@/utils/logger', () => ({
|
|
31
|
+
createLogger: () => ({
|
|
32
|
+
debug: vi.fn(),
|
|
33
|
+
error: vi.fn(),
|
|
34
|
+
info: vi.fn(),
|
|
35
|
+
warn: vi.fn(),
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock LOCAL_STORAGE_URL_PREFIX
|
|
40
|
+
vi.mock('@/const/dir', () => ({
|
|
41
|
+
LOCAL_STORAGE_URL_PREFIX: '/lobe-desktop-file',
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe('StaticFileServerManager', () => {
|
|
45
|
+
let manager: StaticFileServerManager;
|
|
46
|
+
let mockApp: App;
|
|
47
|
+
let mockFileService: { getFile: ReturnType<typeof vi.fn> };
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
|
|
52
|
+
// Reset server handler
|
|
53
|
+
mockServerHandler.current = null;
|
|
54
|
+
|
|
55
|
+
// Reset getPort mock to default behavior
|
|
56
|
+
vi.mocked(getPort).mockResolvedValue(33250);
|
|
57
|
+
|
|
58
|
+
// Reset server mock behaviors
|
|
59
|
+
mockServer.listen.mockImplementation((_port: number, _host: string, cb: () => void) => cb());
|
|
60
|
+
mockServer.close.mockImplementation((cb?: () => void) => cb?.());
|
|
61
|
+
mockServer.on.mockReset();
|
|
62
|
+
|
|
63
|
+
// Create mock FileService
|
|
64
|
+
mockFileService = {
|
|
65
|
+
getFile: vi.fn().mockResolvedValue({
|
|
66
|
+
content: new ArrayBuffer(10),
|
|
67
|
+
mimeType: 'image/png',
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Create mock App
|
|
72
|
+
mockApp = {
|
|
73
|
+
getService: vi.fn().mockReturnValue(mockFileService),
|
|
74
|
+
} as unknown as App;
|
|
75
|
+
|
|
76
|
+
manager = new StaticFileServerManager(mockApp);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
// Ensure cleanup
|
|
81
|
+
if ((manager as any).isInitialized) {
|
|
82
|
+
manager.destroy();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('constructor', () => {
|
|
87
|
+
it('should initialize with app reference and get FileService', () => {
|
|
88
|
+
expect(mockApp.getService).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('initialize', () => {
|
|
93
|
+
it('should get available port and start HTTP server', async () => {
|
|
94
|
+
await manager.initialize();
|
|
95
|
+
|
|
96
|
+
expect(getPort).toHaveBeenCalledWith({
|
|
97
|
+
host: '127.0.0.1',
|
|
98
|
+
port: 33_250,
|
|
99
|
+
ports: [33_251, 33_252, 33_253, 33_254, 33_255],
|
|
100
|
+
});
|
|
101
|
+
expect(createServer).toHaveBeenCalled();
|
|
102
|
+
expect(mockServer.listen).toHaveBeenCalledWith(33250, '127.0.0.1', expect.any(Function));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should skip initialization if already initialized', async () => {
|
|
106
|
+
await manager.initialize();
|
|
107
|
+
|
|
108
|
+
// Clear mocks after first initialization
|
|
109
|
+
vi.mocked(getPort).mockClear();
|
|
110
|
+
vi.mocked(createServer).mockClear();
|
|
111
|
+
|
|
112
|
+
await manager.initialize();
|
|
113
|
+
|
|
114
|
+
expect(getPort).not.toHaveBeenCalled();
|
|
115
|
+
expect(createServer).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should throw error when port acquisition fails', async () => {
|
|
119
|
+
const error = new Error('No available port');
|
|
120
|
+
vi.mocked(getPort).mockRejectedValue(error);
|
|
121
|
+
|
|
122
|
+
await expect(manager.initialize()).rejects.toThrow('No available port');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle server startup error', async () => {
|
|
126
|
+
const serverError = new Error('Address in use');
|
|
127
|
+
|
|
128
|
+
// Mock server.on to capture error handler
|
|
129
|
+
let errorHandler: ((err: Error) => void) | undefined;
|
|
130
|
+
mockServer.on.mockImplementation((event: string, handler: any) => {
|
|
131
|
+
if (event === 'error') {
|
|
132
|
+
errorHandler = handler;
|
|
133
|
+
}
|
|
134
|
+
return mockServer;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Mock listen to not call callback but trigger error
|
|
138
|
+
mockServer.listen.mockImplementation(() => {
|
|
139
|
+
// Trigger error after a tick
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
if (errorHandler) {
|
|
142
|
+
errorHandler(serverError);
|
|
143
|
+
}
|
|
144
|
+
}, 0);
|
|
145
|
+
return mockServer;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await expect(manager.initialize()).rejects.toThrow('Address in use');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('HTTP request handling', () => {
|
|
153
|
+
beforeEach(async () => {
|
|
154
|
+
// Reset mock server behavior
|
|
155
|
+
mockServer.listen.mockImplementation((_port, _host, cb) => cb());
|
|
156
|
+
await manager.initialize();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle OPTIONS request with CORS headers', async () => {
|
|
160
|
+
const req = {
|
|
161
|
+
headers: { origin: 'http://localhost:3000' },
|
|
162
|
+
method: 'OPTIONS',
|
|
163
|
+
on: vi.fn(),
|
|
164
|
+
setTimeout: vi.fn(),
|
|
165
|
+
url: '/lobe-desktop-file/test.png',
|
|
166
|
+
};
|
|
167
|
+
const res = {
|
|
168
|
+
destroyed: false,
|
|
169
|
+
end: vi.fn(),
|
|
170
|
+
headersSent: false,
|
|
171
|
+
writeHead: vi.fn(),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
await mockServerHandler.current(req, res);
|
|
175
|
+
|
|
176
|
+
expect(res.writeHead).toHaveBeenCalledWith(204, {
|
|
177
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
178
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
179
|
+
'Access-Control-Allow-Origin': 'http://localhost:3000',
|
|
180
|
+
'Access-Control-Max-Age': '86400',
|
|
181
|
+
});
|
|
182
|
+
expect(res.end).toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should serve file with correct content type and CORS headers', async () => {
|
|
186
|
+
const fileContent = new ArrayBuffer(100);
|
|
187
|
+
mockFileService.getFile.mockResolvedValue({
|
|
188
|
+
content: fileContent,
|
|
189
|
+
mimeType: 'image/jpeg',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const req = {
|
|
193
|
+
headers: { origin: 'http://127.0.0.1:3000' },
|
|
194
|
+
method: 'GET',
|
|
195
|
+
on: vi.fn(),
|
|
196
|
+
setTimeout: vi.fn(),
|
|
197
|
+
url: '/lobe-desktop-file/images/test.jpg',
|
|
198
|
+
};
|
|
199
|
+
const res = {
|
|
200
|
+
destroyed: false,
|
|
201
|
+
end: vi.fn(),
|
|
202
|
+
headersSent: false,
|
|
203
|
+
writeHead: vi.fn(),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await mockServerHandler.current(req, res);
|
|
207
|
+
|
|
208
|
+
expect(mockFileService.getFile).toHaveBeenCalledWith('desktop://images/test.jpg');
|
|
209
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, {
|
|
210
|
+
'Access-Control-Allow-Origin': 'http://127.0.0.1:3000',
|
|
211
|
+
'Cache-Control': 'public, max-age=31536000',
|
|
212
|
+
'Content-Length': expect.any(Number),
|
|
213
|
+
'Content-Type': 'image/jpeg',
|
|
214
|
+
});
|
|
215
|
+
expect(res.end).toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should return 400 for empty file path', async () => {
|
|
219
|
+
const req = {
|
|
220
|
+
headers: {},
|
|
221
|
+
method: 'GET',
|
|
222
|
+
on: vi.fn(),
|
|
223
|
+
setTimeout: vi.fn(),
|
|
224
|
+
url: '/lobe-desktop-file/',
|
|
225
|
+
};
|
|
226
|
+
const res = {
|
|
227
|
+
destroyed: false,
|
|
228
|
+
end: vi.fn(),
|
|
229
|
+
headersSent: false,
|
|
230
|
+
writeHead: vi.fn(),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
await mockServerHandler.current(req, res);
|
|
234
|
+
|
|
235
|
+
expect(res.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'text/plain' });
|
|
236
|
+
expect(res.end).toHaveBeenCalledWith('Bad Request: Empty file path');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return 404 when file not found', async () => {
|
|
240
|
+
const notFoundError = new Error('File not found');
|
|
241
|
+
notFoundError.name = 'FileNotFoundError';
|
|
242
|
+
mockFileService.getFile.mockRejectedValue(notFoundError);
|
|
243
|
+
|
|
244
|
+
const req = {
|
|
245
|
+
headers: { origin: 'http://localhost:3000' },
|
|
246
|
+
method: 'GET',
|
|
247
|
+
on: vi.fn(),
|
|
248
|
+
setTimeout: vi.fn(),
|
|
249
|
+
url: '/lobe-desktop-file/nonexistent.png',
|
|
250
|
+
};
|
|
251
|
+
const res = {
|
|
252
|
+
destroyed: false,
|
|
253
|
+
end: vi.fn(),
|
|
254
|
+
headersSent: false,
|
|
255
|
+
writeHead: vi.fn(),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
await mockServerHandler.current(req, res);
|
|
259
|
+
|
|
260
|
+
expect(res.writeHead).toHaveBeenCalledWith(404, {
|
|
261
|
+
'Access-Control-Allow-Origin': 'http://localhost:3000',
|
|
262
|
+
'Content-Type': 'text/plain',
|
|
263
|
+
});
|
|
264
|
+
expect(res.end).toHaveBeenCalledWith('File Not Found');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return 500 for server errors', async () => {
|
|
268
|
+
mockFileService.getFile.mockRejectedValue(new Error('Database error'));
|
|
269
|
+
|
|
270
|
+
const req = {
|
|
271
|
+
headers: {},
|
|
272
|
+
method: 'GET',
|
|
273
|
+
on: vi.fn(),
|
|
274
|
+
setTimeout: vi.fn(),
|
|
275
|
+
url: '/lobe-desktop-file/test.png',
|
|
276
|
+
};
|
|
277
|
+
const res = {
|
|
278
|
+
destroyed: false,
|
|
279
|
+
end: vi.fn(),
|
|
280
|
+
headersSent: false,
|
|
281
|
+
writeHead: vi.fn(),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
await mockServerHandler.current(req, res);
|
|
285
|
+
|
|
286
|
+
expect(res.writeHead).toHaveBeenCalledWith(500, {
|
|
287
|
+
'Access-Control-Allow-Origin': '*',
|
|
288
|
+
'Content-Type': 'text/plain',
|
|
289
|
+
});
|
|
290
|
+
expect(res.end).toHaveBeenCalledWith('Internal Server Error');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should skip processing if response is already destroyed', async () => {
|
|
294
|
+
const req = {
|
|
295
|
+
headers: {},
|
|
296
|
+
method: 'GET',
|
|
297
|
+
on: vi.fn(),
|
|
298
|
+
setTimeout: vi.fn(),
|
|
299
|
+
url: '/lobe-desktop-file/test.png',
|
|
300
|
+
};
|
|
301
|
+
const res = {
|
|
302
|
+
destroyed: true,
|
|
303
|
+
end: vi.fn(),
|
|
304
|
+
headersSent: false,
|
|
305
|
+
writeHead: vi.fn(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
await mockServerHandler.current(req, res);
|
|
309
|
+
|
|
310
|
+
expect(res.writeHead).not.toHaveBeenCalled();
|
|
311
|
+
expect(res.end).not.toHaveBeenCalled();
|
|
312
|
+
expect(mockFileService.getFile).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should handle URL-encoded file paths', async () => {
|
|
316
|
+
const req = {
|
|
317
|
+
headers: {},
|
|
318
|
+
method: 'GET',
|
|
319
|
+
on: vi.fn(),
|
|
320
|
+
setTimeout: vi.fn(),
|
|
321
|
+
url: '/lobe-desktop-file/path%20with%20spaces/file%20name.png',
|
|
322
|
+
};
|
|
323
|
+
const res = {
|
|
324
|
+
destroyed: false,
|
|
325
|
+
end: vi.fn(),
|
|
326
|
+
headersSent: false,
|
|
327
|
+
writeHead: vi.fn(),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await mockServerHandler.current(req, res);
|
|
331
|
+
|
|
332
|
+
expect(mockFileService.getFile).toHaveBeenCalledWith(
|
|
333
|
+
'desktop://path with spaces/file name.png',
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('CORS handling', () => {
|
|
339
|
+
beforeEach(async () => {
|
|
340
|
+
mockServer.listen.mockImplementation((_port, _host, cb) => cb());
|
|
341
|
+
await manager.initialize();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should return specific origin for localhost', async () => {
|
|
345
|
+
const req = {
|
|
346
|
+
headers: { origin: 'http://localhost:3000' },
|
|
347
|
+
method: 'OPTIONS',
|
|
348
|
+
on: vi.fn(),
|
|
349
|
+
setTimeout: vi.fn(),
|
|
350
|
+
url: '/test',
|
|
351
|
+
};
|
|
352
|
+
const res = {
|
|
353
|
+
destroyed: false,
|
|
354
|
+
end: vi.fn(),
|
|
355
|
+
headersSent: false,
|
|
356
|
+
writeHead: vi.fn(),
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
await mockServerHandler.current(req, res);
|
|
360
|
+
|
|
361
|
+
expect(res.writeHead).toHaveBeenCalledWith(
|
|
362
|
+
204,
|
|
363
|
+
expect.objectContaining({
|
|
364
|
+
'Access-Control-Allow-Origin': 'http://localhost:3000',
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should return specific origin for 127.0.0.1', async () => {
|
|
370
|
+
const req = {
|
|
371
|
+
headers: { origin: 'http://127.0.0.1:8080' },
|
|
372
|
+
method: 'OPTIONS',
|
|
373
|
+
on: vi.fn(),
|
|
374
|
+
setTimeout: vi.fn(),
|
|
375
|
+
url: '/test',
|
|
376
|
+
};
|
|
377
|
+
const res = {
|
|
378
|
+
destroyed: false,
|
|
379
|
+
end: vi.fn(),
|
|
380
|
+
headersSent: false,
|
|
381
|
+
writeHead: vi.fn(),
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
await mockServerHandler.current(req, res);
|
|
385
|
+
|
|
386
|
+
expect(res.writeHead).toHaveBeenCalledWith(
|
|
387
|
+
204,
|
|
388
|
+
expect.objectContaining({
|
|
389
|
+
'Access-Control-Allow-Origin': 'http://127.0.0.1:8080',
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should return * for other origins', async () => {
|
|
395
|
+
const req = {
|
|
396
|
+
headers: { origin: 'https://example.com' },
|
|
397
|
+
method: 'OPTIONS',
|
|
398
|
+
on: vi.fn(),
|
|
399
|
+
setTimeout: vi.fn(),
|
|
400
|
+
url: '/test',
|
|
401
|
+
};
|
|
402
|
+
const res = {
|
|
403
|
+
destroyed: false,
|
|
404
|
+
end: vi.fn(),
|
|
405
|
+
headersSent: false,
|
|
406
|
+
writeHead: vi.fn(),
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
await mockServerHandler.current(req, res);
|
|
410
|
+
|
|
411
|
+
expect(res.writeHead).toHaveBeenCalledWith(
|
|
412
|
+
204,
|
|
413
|
+
expect.objectContaining({
|
|
414
|
+
'Access-Control-Allow-Origin': '*',
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should return * for no origin', async () => {
|
|
420
|
+
const req = {
|
|
421
|
+
headers: {},
|
|
422
|
+
method: 'OPTIONS',
|
|
423
|
+
on: vi.fn(),
|
|
424
|
+
setTimeout: vi.fn(),
|
|
425
|
+
url: '/test',
|
|
426
|
+
};
|
|
427
|
+
const res = {
|
|
428
|
+
destroyed: false,
|
|
429
|
+
end: vi.fn(),
|
|
430
|
+
headersSent: false,
|
|
431
|
+
writeHead: vi.fn(),
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
await mockServerHandler.current(req, res);
|
|
435
|
+
|
|
436
|
+
expect(res.writeHead).toHaveBeenCalledWith(
|
|
437
|
+
204,
|
|
438
|
+
expect.objectContaining({
|
|
439
|
+
'Access-Control-Allow-Origin': '*',
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('getFileServerDomain', () => {
|
|
446
|
+
it('should return correct domain when initialized', async () => {
|
|
447
|
+
mockServer.listen.mockImplementation((_port, _host, cb) => cb());
|
|
448
|
+
await manager.initialize();
|
|
449
|
+
|
|
450
|
+
const domain = manager.getFileServerDomain();
|
|
451
|
+
|
|
452
|
+
expect(domain).toBe('http://127.0.0.1:33250');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('should throw error when not initialized', () => {
|
|
456
|
+
expect(() => manager.getFileServerDomain()).toThrow(
|
|
457
|
+
'StaticFileServerManager not initialized or server not started',
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('destroy', () => {
|
|
463
|
+
it('should close server and reset state', async () => {
|
|
464
|
+
mockServer.listen.mockImplementation((_port, _host, cb) => cb());
|
|
465
|
+
await manager.initialize();
|
|
466
|
+
|
|
467
|
+
manager.destroy();
|
|
468
|
+
|
|
469
|
+
expect(mockServer.close).toHaveBeenCalled();
|
|
470
|
+
expect((manager as any).httpServer).toBeNull();
|
|
471
|
+
expect((manager as any).serverPort).toBe(0);
|
|
472
|
+
expect((manager as any).isInitialized).toBe(false);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should do nothing if not initialized', () => {
|
|
476
|
+
manager.destroy();
|
|
477
|
+
|
|
478
|
+
expect(mockServer.close).not.toHaveBeenCalled();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { App as AppCore } from '../../App';
|
|
4
|
+
import { StoreManager } from '../StoreManager';
|
|
5
|
+
|
|
6
|
+
// Use vi.hoisted to define mocks before hoisting
|
|
7
|
+
const { mockStoreInstance, mockMakeSureDirExist, MockStore } = vi.hoisted(() => {
|
|
8
|
+
const mockStoreInstance = {
|
|
9
|
+
clear: vi.fn(),
|
|
10
|
+
delete: vi.fn(),
|
|
11
|
+
get: vi.fn().mockImplementation((key: string, defaultValue?: any) => {
|
|
12
|
+
if (key === 'storagePath') return '/mock/storage/path';
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}),
|
|
15
|
+
has: vi.fn().mockReturnValue(false),
|
|
16
|
+
openInEditor: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
set: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MockStore = vi.fn().mockImplementation(() => mockStoreInstance);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
MockStore,
|
|
24
|
+
mockMakeSureDirExist: vi.fn(),
|
|
25
|
+
mockStoreInstance,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Mock electron-store
|
|
30
|
+
vi.mock('electron-store', () => ({
|
|
31
|
+
default: MockStore,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// Mock logger
|
|
35
|
+
vi.mock('@/utils/logger', () => ({
|
|
36
|
+
createLogger: () => ({
|
|
37
|
+
debug: vi.fn(),
|
|
38
|
+
error: vi.fn(),
|
|
39
|
+
info: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mock file-system utils
|
|
45
|
+
vi.mock('@/utils/file-system', () => ({
|
|
46
|
+
makeSureDirExist: mockMakeSureDirExist,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// Mock store constants
|
|
50
|
+
vi.mock('@/const/store', () => ({
|
|
51
|
+
STORE_DEFAULTS: {
|
|
52
|
+
locale: 'auto',
|
|
53
|
+
storagePath: '/default/storage/path',
|
|
54
|
+
},
|
|
55
|
+
STORE_NAME: 'test-config',
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
describe('StoreManager', () => {
|
|
59
|
+
let manager: StoreManager;
|
|
60
|
+
let mockAppCore: AppCore;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
|
|
65
|
+
// Reset store mock behaviors
|
|
66
|
+
mockStoreInstance.get.mockImplementation((key: string, defaultValue?: any) => {
|
|
67
|
+
if (key === 'storagePath') return '/mock/storage/path';
|
|
68
|
+
return defaultValue;
|
|
69
|
+
});
|
|
70
|
+
mockStoreInstance.has.mockReturnValue(false);
|
|
71
|
+
|
|
72
|
+
// Create mock App core
|
|
73
|
+
mockAppCore = {} as unknown as AppCore;
|
|
74
|
+
|
|
75
|
+
manager = new StoreManager(mockAppCore);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('constructor', () => {
|
|
79
|
+
it('should create electron-store with correct options', () => {
|
|
80
|
+
expect(MockStore).toHaveBeenCalledWith({
|
|
81
|
+
defaults: {
|
|
82
|
+
locale: 'auto',
|
|
83
|
+
storagePath: '/default/storage/path',
|
|
84
|
+
},
|
|
85
|
+
name: 'test-config',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should ensure storage directory exists', () => {
|
|
90
|
+
expect(mockMakeSureDirExist).toHaveBeenCalledWith('/mock/storage/path');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('get', () => {
|
|
95
|
+
it('should call store.get with key', () => {
|
|
96
|
+
mockStoreInstance.get.mockReturnValue('test-value');
|
|
97
|
+
|
|
98
|
+
const result = manager.get('locale' as any);
|
|
99
|
+
|
|
100
|
+
expect(mockStoreInstance.get).toHaveBeenCalledWith('locale', undefined);
|
|
101
|
+
expect(result).toBe('test-value');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should call store.get with key and default value', () => {
|
|
105
|
+
mockStoreInstance.get.mockImplementation((_key: string, defaultValue?: any) => defaultValue);
|
|
106
|
+
|
|
107
|
+
const result = manager.get('locale' as any, 'en-US' as any);
|
|
108
|
+
|
|
109
|
+
expect(mockStoreInstance.get).toHaveBeenCalledWith('locale', 'en-US');
|
|
110
|
+
expect(result).toBe('en-US');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('set', () => {
|
|
115
|
+
it('should call store.set with key and value', () => {
|
|
116
|
+
manager.set('locale' as any, 'zh-CN' as any);
|
|
117
|
+
|
|
118
|
+
expect(mockStoreInstance.set).toHaveBeenCalledWith('locale', 'zh-CN');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('delete', () => {
|
|
123
|
+
it('should call store.delete with key', () => {
|
|
124
|
+
manager.delete('locale' as any);
|
|
125
|
+
|
|
126
|
+
expect(mockStoreInstance.delete).toHaveBeenCalledWith('locale');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('clear', () => {
|
|
131
|
+
it('should call store.clear', () => {
|
|
132
|
+
manager.clear();
|
|
133
|
+
|
|
134
|
+
expect(mockStoreInstance.clear).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('has', () => {
|
|
139
|
+
it('should return true when key exists', () => {
|
|
140
|
+
mockStoreInstance.has.mockReturnValue(true);
|
|
141
|
+
|
|
142
|
+
const result = manager.has('locale' as any);
|
|
143
|
+
|
|
144
|
+
expect(mockStoreInstance.has).toHaveBeenCalledWith('locale');
|
|
145
|
+
expect(result).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return false when key does not exist', () => {
|
|
149
|
+
mockStoreInstance.has.mockReturnValue(false);
|
|
150
|
+
|
|
151
|
+
const result = manager.has('nonExistent' as any);
|
|
152
|
+
|
|
153
|
+
expect(result).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('openInEditor', () => {
|
|
158
|
+
it('should call store.openInEditor', async () => {
|
|
159
|
+
await manager.openInEditor();
|
|
160
|
+
|
|
161
|
+
expect(mockStoreInstance.openInEditor).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|