librechat-data-provider 0.7.83 → 0.7.85

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.
@@ -0,0 +1,903 @@
1
+ import type {
2
+ ScraperTypes,
3
+ TCustomConfig,
4
+ RerankerTypes,
5
+ SearchProviders,
6
+ TWebSearchConfig,
7
+ } from '../src/config';
8
+ import { webSearchAuth, loadWebSearchAuth, extractWebSearchEnvVars } from '../src/web';
9
+ import { SafeSearchTypes } from '../src/config';
10
+ import { AuthType } from '../src/schemas';
11
+
12
+ // Mock the extractVariableName function
13
+ jest.mock('../src/utils', () => ({
14
+ extractVariableName: (value: string) => {
15
+ if (!value || typeof value !== 'string') return null;
16
+ const match = value.match(/^\${(.+)}$/);
17
+ return match ? match[1] : null;
18
+ },
19
+ }));
20
+
21
+ describe('web.ts', () => {
22
+ describe('extractWebSearchEnvVars', () => {
23
+ it('should return empty array if config is undefined', () => {
24
+ const result = extractWebSearchEnvVars({
25
+ keys: ['serperApiKey', 'jinaApiKey'],
26
+ config: undefined,
27
+ });
28
+
29
+ expect(result).toEqual([]);
30
+ });
31
+
32
+ it('should extract environment variable names from config values', () => {
33
+ const config: Partial<TWebSearchConfig> = {
34
+ serperApiKey: '${SERPER_API_KEY}',
35
+ jinaApiKey: '${JINA_API_KEY}',
36
+ cohereApiKey: 'actual-api-key', // Not in env var format
37
+ safeSearch: SafeSearchTypes.MODERATE,
38
+ };
39
+
40
+ const result = extractWebSearchEnvVars({
41
+ keys: ['serperApiKey', 'jinaApiKey', 'cohereApiKey'],
42
+ config: config as TWebSearchConfig,
43
+ });
44
+
45
+ expect(result).toEqual(['SERPER_API_KEY', 'JINA_API_KEY']);
46
+ });
47
+
48
+ it('should only extract variables for keys that exist in the config', () => {
49
+ const config: Partial<TWebSearchConfig> = {
50
+ serperApiKey: '${SERPER_API_KEY}',
51
+ // firecrawlApiKey is missing
52
+ safeSearch: SafeSearchTypes.MODERATE,
53
+ };
54
+
55
+ const result = extractWebSearchEnvVars({
56
+ keys: ['serperApiKey', 'firecrawlApiKey'],
57
+ config: config as TWebSearchConfig,
58
+ });
59
+
60
+ expect(result).toEqual(['SERPER_API_KEY']);
61
+ });
62
+ });
63
+
64
+ describe('loadWebSearchAuth', () => {
65
+ // Common test variables
66
+ const userId = 'test-user-id';
67
+ let mockLoadAuthValues: jest.Mock;
68
+ let webSearchConfig: TCustomConfig['webSearch'];
69
+
70
+ beforeEach(() => {
71
+ // Reset mocks before each test
72
+ jest.clearAllMocks();
73
+
74
+ // Initialize the mock function
75
+ mockLoadAuthValues = jest.fn();
76
+
77
+ // Initialize a basic webSearchConfig
78
+ webSearchConfig = {
79
+ serperApiKey: '${SERPER_API_KEY}',
80
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
81
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
82
+ jinaApiKey: '${JINA_API_KEY}',
83
+ cohereApiKey: '${COHERE_API_KEY}',
84
+ safeSearch: SafeSearchTypes.MODERATE,
85
+ };
86
+ });
87
+
88
+ it('should return authenticated=true when all required categories are authenticated', async () => {
89
+ // Mock successful authentication for all services
90
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
91
+ const result: Record<string, string> = {};
92
+ authFields.forEach((field) => {
93
+ result[field] =
94
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
95
+ });
96
+ return Promise.resolve(result);
97
+ });
98
+
99
+ const result = await loadWebSearchAuth({
100
+ userId,
101
+ webSearchConfig,
102
+ loadAuthValues: mockLoadAuthValues,
103
+ });
104
+
105
+ expect(result.authenticated).toBe(true);
106
+ expect(result.authTypes).toHaveLength(3); // providers, scrapers, rerankers
107
+ expect(result.authResult).toHaveProperty('serperApiKey', 'test-api-key');
108
+ expect(result.authResult).toHaveProperty('firecrawlApiKey', 'test-api-key');
109
+
110
+ // The implementation only includes one reranker in the result
111
+ // It will be either jina or cohere, but not both
112
+ if (result.authResult.rerankerType === 'jina') {
113
+ expect(result.authResult).toHaveProperty('jinaApiKey', 'test-api-key');
114
+ } else {
115
+ expect(result.authResult).toHaveProperty('cohereApiKey', 'test-api-key');
116
+ }
117
+
118
+ expect(result.authResult).toHaveProperty('searchProvider', 'serper');
119
+ expect(result.authResult).toHaveProperty('scraperType', 'firecrawl');
120
+ expect(['jina', 'cohere']).toContain(result.authResult.rerankerType as string);
121
+ });
122
+
123
+ it('should return authenticated=false when a required category is not authenticated', async () => {
124
+ // Mock authentication failure for the providers category
125
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
126
+ const result: Record<string, string> = {};
127
+ authFields.forEach((field) => {
128
+ // Only provide values for scrapers and rerankers, not for providers
129
+ if (field !== 'SERPER_API_KEY') {
130
+ result[field] =
131
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
132
+ }
133
+ });
134
+ return Promise.resolve(result);
135
+ });
136
+
137
+ const result = await loadWebSearchAuth({
138
+ userId,
139
+ webSearchConfig,
140
+ loadAuthValues: mockLoadAuthValues,
141
+ });
142
+
143
+ expect(result.authenticated).toBe(false);
144
+ // We should still have authTypes for the categories we checked
145
+ expect(result.authTypes.some(([category]) => category === 'providers')).toBe(true);
146
+ });
147
+
148
+ it('should handle exceptions from loadAuthValues', async () => {
149
+ // Mock loadAuthValues to throw an error
150
+ mockLoadAuthValues.mockImplementation(() => {
151
+ throw new Error('Authentication failed');
152
+ });
153
+
154
+ const result = await loadWebSearchAuth({
155
+ userId,
156
+ webSearchConfig,
157
+ loadAuthValues: mockLoadAuthValues,
158
+ throwError: false, // Don't throw errors
159
+ });
160
+
161
+ expect(result.authenticated).toBe(false);
162
+ });
163
+
164
+ it('should correctly identify user-provided vs system-defined auth', async () => {
165
+ // Mock environment variables
166
+ const originalEnv = process.env;
167
+ process.env = {
168
+ ...originalEnv,
169
+ SERPER_API_KEY: 'system-api-key',
170
+ FIRECRAWL_API_KEY: 'system-api-key',
171
+ JINA_API_KEY: 'system-api-key',
172
+ };
173
+
174
+ // Mock loadAuthValues to return different values for some keys
175
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
176
+ const result: Record<string, string> = {};
177
+ authFields.forEach((field) => {
178
+ if (field === 'SERPER_API_KEY') {
179
+ // This matches the system env var
180
+ result[field] = 'system-api-key';
181
+ } else if (field === 'FIRECRAWL_API_KEY') {
182
+ // This is different from the system env var (user provided)
183
+ result[field] = 'user-api-key';
184
+ } else if (field === 'FIRECRAWL_API_URL') {
185
+ result[field] = 'https://api.firecrawl.dev';
186
+ } else if (field === 'JINA_API_KEY') {
187
+ // This matches the system env var
188
+ result[field] = 'system-api-key';
189
+ } else {
190
+ result[field] = 'test-api-key';
191
+ }
192
+ });
193
+ return Promise.resolve(result);
194
+ });
195
+
196
+ const result = await loadWebSearchAuth({
197
+ userId,
198
+ webSearchConfig,
199
+ loadAuthValues: mockLoadAuthValues,
200
+ });
201
+
202
+ expect(result.authenticated).toBe(true);
203
+ // Check for providers (system-defined) and scrapers (user-provided)
204
+ const providersAuthType = result.authTypes.find(
205
+ ([category]) => category === 'providers',
206
+ )?.[1];
207
+ const scrapersAuthType = result.authTypes.find(([category]) => category === 'scrapers')?.[1];
208
+
209
+ expect(providersAuthType).toBe(AuthType.SYSTEM_DEFINED);
210
+ expect(scrapersAuthType).toBe(AuthType.USER_PROVIDED);
211
+
212
+ // Restore original env
213
+ process.env = originalEnv;
214
+ });
215
+
216
+ it('should handle optional fields correctly', async () => {
217
+ // Create a config without the optional firecrawlApiUrl
218
+ const configWithoutOptional = { ...webSearchConfig } as Partial<TWebSearchConfig>;
219
+ delete configWithoutOptional.firecrawlApiUrl;
220
+
221
+ mockLoadAuthValues.mockImplementation(({ authFields, optional }) => {
222
+ const result: Record<string, string> = {};
223
+ authFields.forEach((field) => {
224
+ // Don't provide values for optional fields
225
+ if (!optional?.has(field)) {
226
+ result[field] = 'test-api-key';
227
+ }
228
+ });
229
+ return Promise.resolve(result);
230
+ });
231
+
232
+ const result = await loadWebSearchAuth({
233
+ userId,
234
+ webSearchConfig: configWithoutOptional as TWebSearchConfig,
235
+ loadAuthValues: mockLoadAuthValues,
236
+ });
237
+
238
+ expect(result.authenticated).toBe(true);
239
+ expect(result.authResult).toHaveProperty('firecrawlApiKey', 'test-api-key');
240
+ // Optional URL should not be in the result
241
+ expect(result.authResult.firecrawlApiUrl).toBeUndefined();
242
+ });
243
+
244
+ it('should preserve safeSearch setting from webSearchConfig', async () => {
245
+ // Mock successful authentication
246
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
247
+ const result: Record<string, string> = {};
248
+ authFields.forEach((field) => {
249
+ result[field] = 'test-api-key';
250
+ });
251
+ return Promise.resolve(result);
252
+ });
253
+
254
+ // Test with safeSearch: OFF
255
+ const configWithSafeSearchOff = {
256
+ ...webSearchConfig,
257
+ safeSearch: SafeSearchTypes.OFF,
258
+ } as TWebSearchConfig;
259
+
260
+ const result = await loadWebSearchAuth({
261
+ userId,
262
+ webSearchConfig: configWithSafeSearchOff,
263
+ loadAuthValues: mockLoadAuthValues,
264
+ });
265
+
266
+ expect(result.authResult).toHaveProperty('safeSearch', SafeSearchTypes.OFF);
267
+ });
268
+
269
+ it('should set the correct service types in authResult', async () => {
270
+ // Mock successful authentication
271
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
272
+ const result: Record<string, string> = {};
273
+ authFields.forEach((field) => {
274
+ result[field] =
275
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
276
+ });
277
+ return Promise.resolve(result);
278
+ });
279
+
280
+ const result = await loadWebSearchAuth({
281
+ userId,
282
+ webSearchConfig,
283
+ loadAuthValues: mockLoadAuthValues,
284
+ });
285
+
286
+ // Check that the correct service types are set
287
+ expect(result.authResult.searchProvider).toBe('serper' as SearchProviders);
288
+ expect(result.authResult.scraperType).toBe('firecrawl' as ScraperTypes);
289
+ // One of the rerankers should be set
290
+ expect(['jina', 'cohere']).toContain(result.authResult.rerankerType as string);
291
+ });
292
+
293
+ it('should check all services if none are specified', async () => {
294
+ // Initialize a webSearchConfig without specific services
295
+ const webSearchConfig: TCustomConfig['webSearch'] = {
296
+ serperApiKey: '${SERPER_API_KEY}',
297
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
298
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
299
+ jinaApiKey: '${JINA_API_KEY}',
300
+ cohereApiKey: '${COHERE_API_KEY}',
301
+ safeSearch: SafeSearchTypes.MODERATE,
302
+ };
303
+
304
+ // Mock successful authentication
305
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
306
+ const result: Record<string, string> = {};
307
+ authFields.forEach((field) => {
308
+ result[field] =
309
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
310
+ });
311
+ return Promise.resolve(result);
312
+ });
313
+
314
+ const result = await loadWebSearchAuth({
315
+ userId,
316
+ webSearchConfig,
317
+ loadAuthValues: mockLoadAuthValues,
318
+ });
319
+
320
+ expect(result.authenticated).toBe(true);
321
+
322
+ // Should have checked all categories
323
+ expect(result.authTypes).toHaveLength(3);
324
+
325
+ // Should have set values for all categories
326
+ expect(result.authResult.searchProvider).toBeDefined();
327
+ expect(result.authResult.scraperType).toBeDefined();
328
+ expect(result.authResult.rerankerType).toBeDefined();
329
+ });
330
+
331
+ it('should correctly identify authTypes based on specific configurations', async () => {
332
+ // Set up environment variables for system-defined auth
333
+ const originalEnv = process.env;
334
+ process.env = {
335
+ ...originalEnv,
336
+ SERPER_API_KEY: 'system-serper-key',
337
+ FIRECRAWL_API_KEY: 'system-firecrawl-key',
338
+ FIRECRAWL_API_URL: 'https://api.firecrawl.dev',
339
+ JINA_API_KEY: 'system-jina-key',
340
+ COHERE_API_KEY: 'system-cohere-key',
341
+ };
342
+
343
+ // Initialize webSearchConfig with environment variable references
344
+ const webSearchConfig: TCustomConfig['webSearch'] = {
345
+ serperApiKey: '${SERPER_API_KEY}',
346
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
347
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
348
+ jinaApiKey: '${JINA_API_KEY}',
349
+ cohereApiKey: '${COHERE_API_KEY}',
350
+ safeSearch: SafeSearchTypes.MODERATE,
351
+ // Specify which services to use
352
+ searchProvider: 'serper' as SearchProviders,
353
+ scraperType: 'firecrawl' as ScraperTypes,
354
+ rerankerType: 'jina' as RerankerTypes,
355
+ };
356
+
357
+ // Mock loadAuthValues to return the actual values
358
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
359
+ const result: Record<string, string> = {};
360
+ authFields.forEach((field) => {
361
+ if (field === 'SERPER_API_KEY') {
362
+ result[field] = 'system-serper-key';
363
+ } else if (field === 'FIRECRAWL_API_KEY') {
364
+ result[field] = 'system-firecrawl-key';
365
+ } else if (field === 'FIRECRAWL_API_URL') {
366
+ result[field] = 'https://api.firecrawl.dev';
367
+ } else if (field === 'JINA_API_KEY') {
368
+ result[field] = 'system-jina-key';
369
+ } else if (field === 'COHERE_API_KEY') {
370
+ result[field] = 'system-cohere-key';
371
+ }
372
+ });
373
+ return Promise.resolve(result);
374
+ });
375
+
376
+ const result = await loadWebSearchAuth({
377
+ userId,
378
+ webSearchConfig,
379
+ loadAuthValues: mockLoadAuthValues,
380
+ });
381
+
382
+ // Verify that all required fields are present in the authResult
383
+ expect(result.authResult).toHaveProperty('serperApiKey');
384
+ expect(result.authResult).toHaveProperty('firecrawlApiKey');
385
+ expect(result.authResult).toHaveProperty('firecrawlApiUrl');
386
+ expect(result.authResult).toHaveProperty('jinaApiKey');
387
+ expect(result.authResult).toHaveProperty('searchProvider');
388
+ expect(result.authResult).toHaveProperty('scraperType');
389
+ expect(result.authResult).toHaveProperty('rerankerType');
390
+
391
+ expect(result.authenticated).toBe(true);
392
+
393
+ // Verify authTypes for each category
394
+ const providersAuthType = result.authTypes.find(
395
+ ([category]) => category === 'providers',
396
+ )?.[1];
397
+ const scrapersAuthType = result.authTypes.find(([category]) => category === 'scrapers')?.[1];
398
+ const rerankersAuthType = result.authTypes.find(
399
+ ([category]) => category === 'rerankers',
400
+ )?.[1];
401
+
402
+ // All should be system-defined since we're using environment variables
403
+ expect(providersAuthType).toBe(AuthType.SYSTEM_DEFINED);
404
+ expect(scrapersAuthType).toBe(AuthType.SYSTEM_DEFINED);
405
+ expect(rerankersAuthType).toBe(AuthType.SYSTEM_DEFINED);
406
+
407
+ // Verify the authResult contains the correct values
408
+ expect(result.authResult).toHaveProperty('serperApiKey', 'system-serper-key');
409
+ expect(result.authResult).toHaveProperty('firecrawlApiKey', 'system-firecrawl-key');
410
+ expect(result.authResult).toHaveProperty('firecrawlApiUrl', 'https://api.firecrawl.dev');
411
+ expect(result.authResult).toHaveProperty('jinaApiKey', 'system-jina-key');
412
+ expect(result.authResult).toHaveProperty('searchProvider', 'serper');
413
+ expect(result.authResult).toHaveProperty('scraperType', 'firecrawl');
414
+ expect(result.authResult).toHaveProperty('rerankerType', 'jina');
415
+
416
+ // Restore original env
417
+ process.env = originalEnv;
418
+ });
419
+
420
+ it('should handle custom variable names in environment variables', async () => {
421
+ // Set up environment variables with custom names
422
+ const originalEnv = process.env;
423
+ process.env = {
424
+ ...originalEnv,
425
+ CUSTOM_SERPER_KEY: 'custom-serper-key',
426
+ CUSTOM_FIRECRAWL_KEY: 'custom-firecrawl-key',
427
+ CUSTOM_FIRECRAWL_URL: 'https://custom.firecrawl.dev',
428
+ CUSTOM_JINA_KEY: 'custom-jina-key',
429
+ CUSTOM_COHERE_KEY: 'custom-cohere-key',
430
+ };
431
+
432
+ // Initialize webSearchConfig with custom variable names
433
+ const webSearchConfig: TCustomConfig['webSearch'] = {
434
+ serperApiKey: '${CUSTOM_SERPER_KEY}',
435
+ firecrawlApiKey: '${CUSTOM_FIRECRAWL_KEY}',
436
+ firecrawlApiUrl: '${CUSTOM_FIRECRAWL_URL}',
437
+ jinaApiKey: '${CUSTOM_JINA_KEY}',
438
+ cohereApiKey: '${CUSTOM_COHERE_KEY}',
439
+ safeSearch: SafeSearchTypes.MODERATE,
440
+ // Specify which services to use
441
+ searchProvider: 'serper' as SearchProviders,
442
+ scraperType: 'firecrawl' as ScraperTypes,
443
+ rerankerType: 'jina' as RerankerTypes, // Only Jina will be checked
444
+ };
445
+
446
+ // Mock loadAuthValues to return the actual values
447
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
448
+ const result: Record<string, string> = {};
449
+ authFields.forEach((field) => {
450
+ if (field === 'CUSTOM_SERPER_KEY') {
451
+ result[field] = 'custom-serper-key';
452
+ } else if (field === 'CUSTOM_FIRECRAWL_KEY') {
453
+ result[field] = 'custom-firecrawl-key';
454
+ } else if (field === 'CUSTOM_FIRECRAWL_URL') {
455
+ result[field] = 'https://custom.firecrawl.dev';
456
+ } else if (field === 'CUSTOM_JINA_KEY') {
457
+ result[field] = 'custom-jina-key';
458
+ }
459
+ // Note: CUSTOM_COHERE_KEY is not checked because we specified jina as rerankerType
460
+ });
461
+ return Promise.resolve(result);
462
+ });
463
+
464
+ const result = await loadWebSearchAuth({
465
+ userId,
466
+ webSearchConfig,
467
+ loadAuthValues: mockLoadAuthValues,
468
+ });
469
+
470
+ expect(result.authenticated).toBe(true);
471
+
472
+ // Verify the authResult contains the correct values from custom variables
473
+ expect(result.authResult).toHaveProperty('serperApiKey', 'custom-serper-key');
474
+ expect(result.authResult).toHaveProperty('firecrawlApiKey', 'custom-firecrawl-key');
475
+ expect(result.authResult).toHaveProperty('firecrawlApiUrl', 'https://custom.firecrawl.dev');
476
+ expect(result.authResult).toHaveProperty('jinaApiKey', 'custom-jina-key');
477
+ // cohereApiKey should not be in the result since we specified jina as rerankerType
478
+ expect(result.authResult).not.toHaveProperty('cohereApiKey');
479
+
480
+ // Verify the service types are set correctly
481
+ expect(result.authResult).toHaveProperty('searchProvider', 'serper');
482
+ expect(result.authResult).toHaveProperty('scraperType', 'firecrawl');
483
+ expect(result.authResult).toHaveProperty('rerankerType', 'jina');
484
+
485
+ // Restore original env
486
+ process.env = originalEnv;
487
+ });
488
+
489
+ it('should always return authTypes array with exactly 3 categories', async () => {
490
+ // Set up environment variables
491
+ const originalEnv = process.env;
492
+ process.env = {
493
+ ...originalEnv,
494
+ SERPER_API_KEY: 'test-key',
495
+ FIRECRAWL_API_KEY: 'test-key',
496
+ FIRECRAWL_API_URL: 'https://api.firecrawl.dev',
497
+ JINA_API_KEY: 'test-key',
498
+ };
499
+
500
+ // Initialize webSearchConfig with environment variable references
501
+ const webSearchConfig: TCustomConfig['webSearch'] = {
502
+ serperApiKey: '${SERPER_API_KEY}',
503
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
504
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
505
+ jinaApiKey: '${JINA_API_KEY}',
506
+ cohereApiKey: '${COHERE_API_KEY}',
507
+ safeSearch: SafeSearchTypes.MODERATE,
508
+ };
509
+
510
+ // Mock loadAuthValues to return values
511
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
512
+ const result: Record<string, string> = {};
513
+ authFields.forEach((field) => {
514
+ result[field] = field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-key';
515
+ });
516
+ return Promise.resolve(result);
517
+ });
518
+
519
+ const result = await loadWebSearchAuth({
520
+ userId,
521
+ webSearchConfig,
522
+ loadAuthValues: mockLoadAuthValues,
523
+ });
524
+
525
+ // Get the number of categories from webSearchAuth
526
+ const expectedCategoryCount = Object.keys(webSearchAuth).length;
527
+
528
+ // Verify authTypes array structure
529
+ expect(result.authTypes).toHaveLength(expectedCategoryCount);
530
+
531
+ // Verify each category exists exactly once
532
+ const categories = result.authTypes.map(([category]) => category);
533
+ Object.keys(webSearchAuth).forEach((category) => {
534
+ expect(categories).toContain(category);
535
+ });
536
+
537
+ // Verify no duplicate categories
538
+ expect(new Set(categories).size).toBe(expectedCategoryCount);
539
+
540
+ // Verify each entry has the correct format [category, AuthType]
541
+ result.authTypes.forEach(([category, authType]) => {
542
+ expect(typeof category).toBe('string');
543
+ expect([AuthType.SYSTEM_DEFINED, AuthType.USER_PROVIDED]).toContain(authType);
544
+ });
545
+
546
+ // Restore original env
547
+ process.env = originalEnv;
548
+ });
549
+
550
+ it('should maintain authTypes array structure even when authentication fails', async () => {
551
+ // Set up environment variables
552
+ const originalEnv = process.env;
553
+ process.env = {
554
+ ...originalEnv,
555
+ SERPER_API_KEY: 'test-key',
556
+ // Missing other keys to force authentication failure
557
+ };
558
+
559
+ // Initialize webSearchConfig with environment variable references
560
+ const webSearchConfig: TCustomConfig['webSearch'] = {
561
+ serperApiKey: '${SERPER_API_KEY}',
562
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
563
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
564
+ jinaApiKey: '${JINA_API_KEY}',
565
+ cohereApiKey: '${COHERE_API_KEY}',
566
+ safeSearch: SafeSearchTypes.MODERATE,
567
+ };
568
+
569
+ // Mock loadAuthValues to return partial values
570
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
571
+ const result: Record<string, string> = {};
572
+ authFields.forEach((field) => {
573
+ if (field === 'SERPER_API_KEY') {
574
+ result[field] = 'test-key';
575
+ }
576
+ // Other fields are intentionally missing
577
+ });
578
+ return Promise.resolve(result);
579
+ });
580
+
581
+ const result = await loadWebSearchAuth({
582
+ userId,
583
+ webSearchConfig,
584
+ loadAuthValues: mockLoadAuthValues,
585
+ });
586
+
587
+ // Get the number of categories from webSearchAuth
588
+ const expectedCategoryCount = Object.keys(webSearchAuth).length;
589
+
590
+ // Verify authentication failed
591
+ expect(result.authenticated).toBe(false);
592
+
593
+ // Verify authTypes array structure is maintained
594
+ expect(result.authTypes).toHaveLength(expectedCategoryCount);
595
+
596
+ // Verify each category exists exactly once
597
+ const categories = result.authTypes.map(([category]) => category);
598
+ Object.keys(webSearchAuth).forEach((category) => {
599
+ expect(categories).toContain(category);
600
+ });
601
+
602
+ // Verify no duplicate categories
603
+ expect(new Set(categories).size).toBe(expectedCategoryCount);
604
+
605
+ // Verify each entry has the correct format [category, AuthType]
606
+ result.authTypes.forEach(([category, authType]) => {
607
+ expect(typeof category).toBe('string');
608
+ expect([AuthType.SYSTEM_DEFINED, AuthType.USER_PROVIDED]).toContain(authType);
609
+ });
610
+
611
+ // Restore original env
612
+ process.env = originalEnv;
613
+ });
614
+ });
615
+
616
+ describe('webSearchAuth', () => {
617
+ it('should have the expected structure', () => {
618
+ // Check that all expected categories exist
619
+ expect(webSearchAuth).toHaveProperty('providers');
620
+ expect(webSearchAuth).toHaveProperty('scrapers');
621
+ expect(webSearchAuth).toHaveProperty('rerankers');
622
+
623
+ // Check providers
624
+ expect(webSearchAuth.providers).toHaveProperty('serper');
625
+ expect(webSearchAuth.providers.serper).toHaveProperty('serperApiKey', 1);
626
+
627
+ // Check scrapers
628
+ expect(webSearchAuth.scrapers).toHaveProperty('firecrawl');
629
+ expect(webSearchAuth.scrapers.firecrawl).toHaveProperty('firecrawlApiKey', 1);
630
+ expect(webSearchAuth.scrapers.firecrawl).toHaveProperty('firecrawlApiUrl', 0);
631
+
632
+ // Check rerankers
633
+ expect(webSearchAuth.rerankers).toHaveProperty('jina');
634
+ expect(webSearchAuth.rerankers.jina).toHaveProperty('jinaApiKey', 1);
635
+ expect(webSearchAuth.rerankers).toHaveProperty('cohere');
636
+ expect(webSearchAuth.rerankers.cohere).toHaveProperty('cohereApiKey', 1);
637
+ });
638
+
639
+ it('should mark required keys with value 1', () => {
640
+ // All keys with value 1 are required
641
+ expect(webSearchAuth.providers.serper.serperApiKey).toBe(1);
642
+ expect(webSearchAuth.scrapers.firecrawl.firecrawlApiKey).toBe(1);
643
+ expect(webSearchAuth.rerankers.jina.jinaApiKey).toBe(1);
644
+ expect(webSearchAuth.rerankers.cohere.cohereApiKey).toBe(1);
645
+ });
646
+
647
+ it('should mark optional keys with value 0', () => {
648
+ // Keys with value 0 are optional
649
+ expect(webSearchAuth.scrapers.firecrawl.firecrawlApiUrl).toBe(0);
650
+ });
651
+ });
652
+ describe('loadWebSearchAuth with specific services', () => {
653
+ // Common test variables
654
+ const userId = 'test-user-id';
655
+ let mockLoadAuthValues: jest.Mock;
656
+
657
+ beforeEach(() => {
658
+ // Reset mocks before each test
659
+ jest.clearAllMocks();
660
+
661
+ // Initialize the mock function
662
+ mockLoadAuthValues = jest.fn();
663
+ });
664
+
665
+ it('should only check the specified searchProvider', async () => {
666
+ // Initialize a webSearchConfig with a specific searchProvider
667
+ const webSearchConfig: TCustomConfig['webSearch'] = {
668
+ serperApiKey: '${SERPER_API_KEY}',
669
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
670
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
671
+ jinaApiKey: '${JINA_API_KEY}',
672
+ cohereApiKey: '${COHERE_API_KEY}',
673
+ safeSearch: SafeSearchTypes.MODERATE,
674
+ searchProvider: 'serper' as SearchProviders,
675
+ };
676
+
677
+ // Mock successful authentication
678
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
679
+ const result: Record<string, string> = {};
680
+ authFields.forEach((field) => {
681
+ result[field] =
682
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
683
+ });
684
+ return Promise.resolve(result);
685
+ });
686
+
687
+ const result = await loadWebSearchAuth({
688
+ userId,
689
+ webSearchConfig,
690
+ loadAuthValues: mockLoadAuthValues,
691
+ });
692
+
693
+ expect(result.authenticated).toBe(true);
694
+ expect(result.authResult.searchProvider).toBe('serper');
695
+
696
+ // Verify that only SERPER_API_KEY was requested for the providers category
697
+ const providerCalls = mockLoadAuthValues.mock.calls.filter((call) =>
698
+ call[0].authFields.includes('SERPER_API_KEY'),
699
+ );
700
+ expect(providerCalls.length).toBe(1);
701
+ });
702
+
703
+ it('should only check the specified scraperType', async () => {
704
+ // Initialize a webSearchConfig with a specific scraperType
705
+ const webSearchConfig: TCustomConfig['webSearch'] = {
706
+ serperApiKey: '${SERPER_API_KEY}',
707
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
708
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
709
+ jinaApiKey: '${JINA_API_KEY}',
710
+ cohereApiKey: '${COHERE_API_KEY}',
711
+ safeSearch: SafeSearchTypes.MODERATE,
712
+ scraperType: 'firecrawl' as ScraperTypes,
713
+ };
714
+
715
+ // Mock successful authentication
716
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
717
+ const result: Record<string, string> = {};
718
+ authFields.forEach((field) => {
719
+ result[field] =
720
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
721
+ });
722
+ return Promise.resolve(result);
723
+ });
724
+
725
+ const result = await loadWebSearchAuth({
726
+ userId,
727
+ webSearchConfig,
728
+ loadAuthValues: mockLoadAuthValues,
729
+ });
730
+
731
+ expect(result.authenticated).toBe(true);
732
+ expect(result.authResult.scraperType).toBe('firecrawl');
733
+
734
+ // Verify that only FIRECRAWL_API_KEY and FIRECRAWL_API_URL were requested for the scrapers category
735
+ const scraperCalls = mockLoadAuthValues.mock.calls.filter((call) =>
736
+ call[0].authFields.includes('FIRECRAWL_API_KEY'),
737
+ );
738
+ expect(scraperCalls.length).toBe(1);
739
+ });
740
+
741
+ it('should only check the specified rerankerType', async () => {
742
+ // Initialize a webSearchConfig with a specific rerankerType
743
+ const webSearchConfig: TCustomConfig['webSearch'] = {
744
+ serperApiKey: '${SERPER_API_KEY}',
745
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
746
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
747
+ jinaApiKey: '${JINA_API_KEY}',
748
+ cohereApiKey: '${COHERE_API_KEY}',
749
+ safeSearch: SafeSearchTypes.MODERATE,
750
+ rerankerType: 'jina' as RerankerTypes,
751
+ };
752
+
753
+ // Mock successful authentication
754
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
755
+ const result: Record<string, string> = {};
756
+ authFields.forEach((field) => {
757
+ result[field] =
758
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
759
+ });
760
+ return Promise.resolve(result);
761
+ });
762
+
763
+ const result = await loadWebSearchAuth({
764
+ userId,
765
+ webSearchConfig,
766
+ loadAuthValues: mockLoadAuthValues,
767
+ });
768
+
769
+ expect(result.authenticated).toBe(true);
770
+ expect(result.authResult.rerankerType).toBe('jina');
771
+
772
+ // Verify that only JINA_API_KEY was requested for the rerankers category
773
+ const rerankerCalls = mockLoadAuthValues.mock.calls.filter((call) =>
774
+ call[0].authFields.includes('JINA_API_KEY'),
775
+ );
776
+ expect(rerankerCalls.length).toBe(1);
777
+
778
+ // Verify that COHERE_API_KEY was not requested
779
+ const cohereCalls = mockLoadAuthValues.mock.calls.filter((call) =>
780
+ call[0].authFields.includes('COHERE_API_KEY'),
781
+ );
782
+ expect(cohereCalls.length).toBe(0);
783
+ });
784
+
785
+ it('should handle invalid specified service gracefully', async () => {
786
+ // Initialize a webSearchConfig with an invalid searchProvider
787
+ const webSearchConfig: TCustomConfig['webSearch'] = {
788
+ serperApiKey: '${SERPER_API_KEY}',
789
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
790
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
791
+ jinaApiKey: '${JINA_API_KEY}',
792
+ cohereApiKey: '${COHERE_API_KEY}',
793
+ safeSearch: SafeSearchTypes.MODERATE,
794
+ searchProvider: 'invalid-provider' as SearchProviders,
795
+ };
796
+
797
+ // Mock successful authentication
798
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
799
+ const result: Record<string, string> = {};
800
+ authFields.forEach((field) => {
801
+ result[field] =
802
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
803
+ });
804
+ return Promise.resolve(result);
805
+ });
806
+
807
+ const result = await loadWebSearchAuth({
808
+ userId,
809
+ webSearchConfig,
810
+ loadAuthValues: mockLoadAuthValues,
811
+ });
812
+
813
+ // Should fail because the specified provider doesn't exist
814
+ expect(result.authenticated).toBe(false);
815
+ });
816
+
817
+ it('should fail authentication when specified service is not authenticated but others are', async () => {
818
+ // Initialize a webSearchConfig with a specific rerankerType (jina)
819
+ const webSearchConfig: TCustomConfig['webSearch'] = {
820
+ serperApiKey: '${SERPER_API_KEY}',
821
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
822
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
823
+ jinaApiKey: '${JINA_API_KEY}',
824
+ cohereApiKey: '${COHERE_API_KEY}',
825
+ safeSearch: SafeSearchTypes.MODERATE,
826
+ rerankerType: 'jina' as RerankerTypes,
827
+ };
828
+
829
+ // Mock authentication where cohere is authenticated but jina is not
830
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
831
+ const result: Record<string, string> = {};
832
+ authFields.forEach((field) => {
833
+ // Authenticate all fields except JINA_API_KEY
834
+ if (field !== 'JINA_API_KEY') {
835
+ result[field] =
836
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
837
+ }
838
+ });
839
+ return Promise.resolve(result);
840
+ });
841
+
842
+ const result = await loadWebSearchAuth({
843
+ userId,
844
+ webSearchConfig,
845
+ loadAuthValues: mockLoadAuthValues,
846
+ });
847
+
848
+ // Should fail because the specified reranker (jina) is not authenticated
849
+ // even though another reranker (cohere) might be authenticated
850
+ expect(result.authenticated).toBe(false);
851
+
852
+ // Verify that JINA_API_KEY was requested
853
+ const jinaApiKeyCalls = mockLoadAuthValues.mock.calls.filter((call) =>
854
+ call[0].authFields.includes('JINA_API_KEY'),
855
+ );
856
+ expect(jinaApiKeyCalls.length).toBe(1);
857
+
858
+ // Verify that COHERE_API_KEY was not requested since we specified jina
859
+ const cohereApiKeyCalls = mockLoadAuthValues.mock.calls.filter((call) =>
860
+ call[0].authFields.includes('COHERE_API_KEY'),
861
+ );
862
+ expect(cohereApiKeyCalls.length).toBe(0);
863
+ });
864
+
865
+ it('should check all services if none are specified', async () => {
866
+ // Initialize a webSearchConfig without specific services
867
+ const webSearchConfig: TCustomConfig['webSearch'] = {
868
+ serperApiKey: '${SERPER_API_KEY}',
869
+ firecrawlApiKey: '${FIRECRAWL_API_KEY}',
870
+ firecrawlApiUrl: '${FIRECRAWL_API_URL}',
871
+ jinaApiKey: '${JINA_API_KEY}',
872
+ cohereApiKey: '${COHERE_API_KEY}',
873
+ safeSearch: SafeSearchTypes.MODERATE,
874
+ };
875
+
876
+ // Mock successful authentication
877
+ mockLoadAuthValues.mockImplementation(({ authFields }) => {
878
+ const result: Record<string, string> = {};
879
+ authFields.forEach((field) => {
880
+ result[field] =
881
+ field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key';
882
+ });
883
+ return Promise.resolve(result);
884
+ });
885
+
886
+ const result = await loadWebSearchAuth({
887
+ userId,
888
+ webSearchConfig,
889
+ loadAuthValues: mockLoadAuthValues,
890
+ });
891
+
892
+ expect(result.authenticated).toBe(true);
893
+
894
+ // Should have checked all categories
895
+ expect(result.authTypes).toHaveLength(3);
896
+
897
+ // Should have set values for all categories
898
+ expect(result.authResult.searchProvider).toBeDefined();
899
+ expect(result.authResult.scraperType).toBeDefined();
900
+ expect(result.authResult.rerankerType).toBeDefined();
901
+ });
902
+ });
903
+ });